From f44209e20d3379a9dda6f3b6b780c83616348b26 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 5 Mar 2020 21:33:00 +0800 Subject: Design the data manager interface. --- Timeline/Services/DataManager.cs | 79 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 Timeline/Services/DataManager.cs (limited to 'Timeline/Services/DataManager.cs') diff --git a/Timeline/Services/DataManager.cs b/Timeline/Services/DataManager.cs new file mode 100644 index 00000000..005ad23c --- /dev/null +++ b/Timeline/Services/DataManager.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Timeline.Services +{ + public class DataEntry + { +#pragma warning disable CA1819 // Properties should not return arrays + public byte[] Data { get; set; } = default!; +#pragma warning restore CA1819 // Properties should not return arrays + public DataInfo Info { get; set; } = default!; + } + + public class DataInfo + { + public DateTime Time { get; set; } + public string Type { get; set; } = default!; + } + + /// + /// A data manager controling data. + /// + /// + /// All data to be saved will be checked identity. + /// Identical data will be saved as one copy and return the same tag. + /// Every data has a ref count. When data is saved, ref count increase. + /// When data is removed, ref count decease. If ref count is decreased + /// to 0, the data entry will be destroyed and no longer occupy space. + /// + /// Type is just an attached attribute for convenience and not participate + /// in identity verification. This should be only used to save blobs but not + /// strings. It will be rare for identity blob with different type, I think. + /// + public interface IDataManager + { + /// + /// Saves the data to a new entry if it does not exist, + /// increases its ref count and returns a tag to the entry. + /// + /// The data. Can't be null. + /// The type of the data. Can't be null. + /// The tag of the created entry. + /// Thrown when or is null. + /// Thrown when a saved copy of data already exists but type is different. + public Task RetainEntry(byte[] data, string type); + + /// + /// Decrease the the ref count of the entry. + /// Remove it if ref count is zero. + /// + /// The tag of the entry. + /// Thrown when is null. + /// + /// It's no-op if entry with tag does not exist. + /// + public Task FreeEntry(string tag); + + /// + /// Retrieve the entry with given tag. + /// + /// The tag of the entry. + /// The entry. + /// Thrown when is null. + /// Thrown when entry with given tag does not exist. + public Task GetEntry(string tag); + + /// + /// Retrieve info of the entry with given tag. + /// + /// The tag of the entry. + /// The entry info. + /// Thrown when is null. + /// Thrown when entry with given tag does not exist. + public Task GetEntryInfo(string tag); + } + +} -- cgit v1.2.3 From 57b29adef10dcefcacf255df05cf6bc11512caf2 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 5 Mar 2020 23:02:22 +0800 Subject: Implement data manager. --- Timeline/Entities/DataEntity.cs | 23 +++++++ Timeline/Entities/DatabaseContext.cs | 1 + Timeline/Services/DataManager.cs | 122 ++++++++++++++++++++++++----------- 3 files changed, 107 insertions(+), 39 deletions(-) create mode 100644 Timeline/Entities/DataEntity.cs (limited to 'Timeline/Services/DataManager.cs') diff --git a/Timeline/Entities/DataEntity.cs b/Timeline/Entities/DataEntity.cs new file mode 100644 index 00000000..b21e2dbf --- /dev/null +++ b/Timeline/Entities/DataEntity.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Timeline.Entities +{ + [Table("data")] + public class DataEntity + { + [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + [Column("tag"), Required] + public string Tag { get; set; } = default!; + + [Column("data"), Required] +#pragma warning disable CA1819 // Properties should not return arrays + public byte[] Data { get; set; } = default!; +#pragma warning restore CA1819 // Properties should not return arrays + + [Column("ref"), Required] + public int Ref { get; set; } + } +} diff --git a/Timeline/Entities/DatabaseContext.cs b/Timeline/Entities/DatabaseContext.cs index 039cbd51..3ed61b71 100644 --- a/Timeline/Entities/DatabaseContext.cs +++ b/Timeline/Entities/DatabaseContext.cs @@ -21,5 +21,6 @@ namespace Timeline.Entities public DbSet TimelinePosts { get; set; } = default!; public DbSet TimelineMembers { get; set; } = default!; public DbSet JwtToken { get; set; } = default!; + public DbSet Data { get; set; } = default!; } } diff --git a/Timeline/Services/DataManager.cs b/Timeline/Services/DataManager.cs index 005ad23c..66aa6f81 100644 --- a/Timeline/Services/DataManager.cs +++ b/Timeline/Services/DataManager.cs @@ -1,37 +1,19 @@ -using System; -using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using System; using System.Linq; using System.Threading.Tasks; +using Timeline.Entities; namespace Timeline.Services { - public class DataEntry - { -#pragma warning disable CA1819 // Properties should not return arrays - public byte[] Data { get; set; } = default!; -#pragma warning restore CA1819 // Properties should not return arrays - public DataInfo Info { get; set; } = default!; - } - - public class DataInfo - { - public DateTime Time { get; set; } - public string Type { get; set; } = default!; - } - /// - /// A data manager controling data. + /// A data manager controlling data. /// /// - /// All data to be saved will be checked identity. /// Identical data will be saved as one copy and return the same tag. - /// Every data has a ref count. When data is saved, ref count increase. - /// When data is removed, ref count decease. If ref count is decreased + /// Every data has a ref count. When data is retained, ref count increase. + /// When data is freed, ref count decease. If ref count is decreased /// to 0, the data entry will be destroyed and no longer occupy space. - /// - /// Type is just an attached attribute for convenience and not participate - /// in identity verification. This should be only used to save blobs but not - /// strings. It will be rare for identity blob with different type, I think. /// public interface IDataManager { @@ -40,11 +22,9 @@ namespace Timeline.Services /// increases its ref count and returns a tag to the entry. /// /// The data. Can't be null. - /// The type of the data. Can't be null. /// The tag of the created entry. - /// Thrown when or is null. - /// Thrown when a saved copy of data already exists but type is different. - public Task RetainEntry(byte[] data, string type); + /// Thrown when is null. + public Task RetainEntry(byte[] data); /// /// Decrease the the ref count of the entry. @@ -61,19 +41,83 @@ namespace Timeline.Services /// Retrieve the entry with given tag. /// /// The tag of the entry. - /// The entry. + /// The data of the entry. /// Thrown when is null. /// Thrown when entry with given tag does not exist. - public Task GetEntry(string tag); - - /// - /// Retrieve info of the entry with given tag. - /// - /// The tag of the entry. - /// The entry info. - /// Thrown when is null. - /// Thrown when entry with given tag does not exist. - public Task GetEntryInfo(string tag); + public Task GetEntry(string tag); } + public class DataManager : IDataManager + { + private readonly DatabaseContext _database; + private readonly IETagGenerator _eTagGenerator; + + public DataManager(DatabaseContext database, IETagGenerator eTagGenerator) + { + _database = database; + _eTagGenerator = eTagGenerator; + } + + public async Task RetainEntry(byte[] data) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + + var tag = await _eTagGenerator.Generate(data); + + var entity = await _database.Data.Where(d => d.Tag == tag).SingleOrDefaultAsync(); + + if (entity == null) + { + entity = new DataEntity + { + Tag = tag, + Data = data, + Ref = 1 + }; + _database.Data.Add(entity); + await _database.SaveChangesAsync(); + } + else + { + entity.Ref += 1; + await _database.SaveChangesAsync(); + } + return tag; + } + + public async Task FreeEntry(string tag) + { + if (tag == null) + throw new ArgumentNullException(nameof(tag)); + + var entity = await _database.Data.Where(d => d.Tag == tag).SingleOrDefaultAsync(); + + if (entity != null) + { + if (entity.Ref == 1) + { + _database.Data.Remove(entity); + } + else + { + entity.Ref -= 1; + } + await _database.SaveChangesAsync(); + } + } + + public async Task GetEntry(string tag) + { + if (tag == null) + throw new ArgumentNullException(nameof(tag)); + + var entity = await _database.Data.Where(d => d.Tag == tag).Select(d => new { d.Data }).SingleOrDefaultAsync(); + + if (entity == null) + throw new InvalidOperationException("Entry with given tag does not exist."); + + return entity.Data; + } + } } -- cgit v1.2.3 From a88ce6495185e7e63c5c1362c7e3f8660b89333d Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 5 Mar 2020 23:27:41 +0800 Subject: Migrate avatar service. TODO: Migrate database. --- Timeline/Entities/UserAvatarEntity.cs | 7 +- .../Resources/Services/DataManager.Designer.cs | 72 ++++++++++++ Timeline/Resources/Services/DataManager.resx | 123 +++++++++++++++++++++ Timeline/Services/DataManager.cs | 2 +- Timeline/Services/UserAvatarService.cs | 43 +++---- Timeline/Timeline.csproj | 9 ++ 6 files changed, 230 insertions(+), 26 deletions(-) create mode 100644 Timeline/Resources/Services/DataManager.Designer.cs create mode 100644 Timeline/Resources/Services/DataManager.resx (limited to 'Timeline/Services/DataManager.cs') diff --git a/Timeline/Entities/UserAvatarEntity.cs b/Timeline/Entities/UserAvatarEntity.cs index be094a77..3c2720f7 100644 --- a/Timeline/Entities/UserAvatarEntity.cs +++ b/Timeline/Entities/UserAvatarEntity.cs @@ -11,15 +11,12 @@ namespace Timeline.Entities [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public long Id { get; set; } - [Column("data")] - public byte[]? Data { get; set; } + [Column("data_tag")] + public string? DataTag { get; set; } [Column("type")] public string? Type { get; set; } - [Column("etag")] - public string? ETag { get; set; } - [Column("last_modified"), Required] public DateTime LastModified { get; set; } diff --git a/Timeline/Resources/Services/DataManager.Designer.cs b/Timeline/Resources/Services/DataManager.Designer.cs new file mode 100644 index 00000000..0872059a --- /dev/null +++ b/Timeline/Resources/Services/DataManager.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Services { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class DataManager { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal DataManager() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.DataManager", typeof(DataManager).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Entry with given tag does not exist.. + /// + internal static string ExceptionEntryNotExist { + get { + return ResourceManager.GetString("ExceptionEntryNotExist", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Services/DataManager.resx b/Timeline/Resources/Services/DataManager.resx new file mode 100644 index 00000000..688e0e96 --- /dev/null +++ b/Timeline/Resources/Services/DataManager.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Entry with given tag does not exist. + + \ No newline at end of file diff --git a/Timeline/Services/DataManager.cs b/Timeline/Services/DataManager.cs index 66aa6f81..d6b8b6a4 100644 --- a/Timeline/Services/DataManager.cs +++ b/Timeline/Services/DataManager.cs @@ -115,7 +115,7 @@ namespace Timeline.Services var entity = await _database.Data.Where(d => d.Tag == tag).Select(d => new { d.Data }).SingleOrDefaultAsync(); if (entity == null) - throw new InvalidOperationException("Entry with given tag does not exist."); + throw new InvalidOperationException(Resources.Services.DataManager.ExceptionEntryNotExist); return entity.Data; } diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs index 39b408e6..5a07da96 100644 --- a/Timeline/Services/UserAvatarService.cs +++ b/Timeline/Services/UserAvatarService.cs @@ -164,7 +164,7 @@ namespace Timeline.Services private readonly IDefaultUserAvatarProvider _defaultUserAvatarProvider; private readonly IUserAvatarValidator _avatarValidator; - private readonly IETagGenerator _eTagGenerator; + private readonly IDataManager _dataManager; private readonly IClock _clock; @@ -173,20 +173,20 @@ namespace Timeline.Services DatabaseContext database, IDefaultUserAvatarProvider defaultUserAvatarProvider, IUserAvatarValidator avatarValidator, - IETagGenerator eTagGenerator, + IDataManager dataManager, IClock clock) { _logger = logger; _database = database; _defaultUserAvatarProvider = defaultUserAvatarProvider; _avatarValidator = avatarValidator; - _eTagGenerator = eTagGenerator; + _dataManager = dataManager; _clock = clock; } public async Task GetAvatarETag(long id) { - var eTag = (await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.ETag }).SingleOrDefaultAsync())?.ETag; + var eTag = (await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.DataTag }).SingleOrDefaultAsync())?.DataTag; if (eTag == null) return await _defaultUserAvatarProvider.GetDefaultAvatarETag(); else @@ -195,25 +195,27 @@ namespace Timeline.Services public async Task GetAvatar(long id) { - var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.Type, a.Data, a.LastModified }).SingleOrDefaultAsync(); + var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == id).Select(a => new { a.Type, a.DataTag, a.LastModified }).SingleOrDefaultAsync(); if (avatarEntity != null) { - if (!LanguageHelper.AreSame(avatarEntity.Data == null, avatarEntity.Type == null)) + if (!LanguageHelper.AreSame(avatarEntity.DataTag == null, avatarEntity.Type == null)) { var message = Resources.Services.UserAvatarService.ExceptionDatabaseCorruptedDataAndTypeNotSame; _logger.LogCritical(message); throw new DatabaseCorruptedException(message); } - if (avatarEntity.Data != null) + + if (avatarEntity.DataTag != null) { + var data = await _dataManager.GetEntry(avatarEntity.DataTag); return new AvatarInfo { Avatar = new Avatar { Type = avatarEntity.Type!, - Data = avatarEntity.Data + Data = data }, LastModified = avatarEntity.LastModified }; @@ -239,15 +241,15 @@ namespace Timeline.Services if (avatar == null) { - if (avatarEntity == null || avatarEntity.Data == null) + if (avatarEntity == null || avatarEntity.DataTag == null) { return; } else { - avatarEntity.Data = null; + await _dataManager.FreeEntry(avatarEntity.DataTag); + avatarEntity.DataTag = null; avatarEntity.Type = null; - avatarEntity.ETag = null; avatarEntity.LastModified = _clock.GetCurrentTime(); await _database.SaveChangesAsync(); _logger.LogInformation(Resources.Services.UserAvatarService.LogUpdateEntity); @@ -256,24 +258,26 @@ namespace Timeline.Services else { await _avatarValidator.Validate(avatar); + var oldTag = avatarEntity?.DataTag; var create = avatarEntity == null; - if (create) + if (avatarEntity == null) { avatarEntity = new UserAvatarEntity(); + _database.UserAvatars.Add(avatarEntity); } - avatarEntity!.Type = avatar.Type; - avatarEntity.Data = avatar.Data; - avatarEntity.ETag = await _eTagGenerator.Generate(avatar.Data); + var tag = await _dataManager.RetainEntry(avatar.Data); + avatarEntity.DataTag = tag; + avatarEntity.Type = avatar.Type; avatarEntity.LastModified = _clock.GetCurrentTime(); avatarEntity.UserId = id; - if (create) - { - _database.UserAvatars.Add(avatarEntity); - } await _database.SaveChangesAsync(); _logger.LogInformation(create ? Resources.Services.UserAvatarService.LogCreateEntity : Resources.Services.UserAvatarService.LogUpdateEntity); + if (oldTag != null) + { + await _dataManager.FreeEntry(oldTag); + } } } } @@ -282,7 +286,6 @@ namespace Timeline.Services { public static void AddUserAvatarService(this IServiceCollection services) { - services.TryAddTransient(); services.AddScoped(); services.AddSingleton(); services.AddTransient(); diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index 2dcc3c9f..72bc1572 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -97,6 +97,11 @@ True Validator.resx + + True + True + DataManager.resx + True True @@ -174,6 +179,10 @@ ResXFileCodeGenerator Validator.Designer.cs + + ResXFileCodeGenerator + DataManager.Designer.cs + ResXFileCodeGenerator Exception.Designer.cs -- cgit v1.2.3 From 81218733d494452e04a1ab84fae242d0bba08092 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 6 Mar 2020 18:12:51 +0800 Subject: ... --- Timeline/Services/DataManager.cs | 3 +-- Timeline/Services/UserAvatarService.cs | 4 ++-- Timeline/Startup.cs | 4 ++++ 3 files changed, 7 insertions(+), 4 deletions(-) (limited to 'Timeline/Services/DataManager.cs') diff --git a/Timeline/Services/DataManager.cs b/Timeline/Services/DataManager.cs index d6b8b6a4..d447b0d5 100644 --- a/Timeline/Services/DataManager.cs +++ b/Timeline/Services/DataManager.cs @@ -76,13 +76,12 @@ namespace Timeline.Services Ref = 1 }; _database.Data.Add(entity); - await _database.SaveChangesAsync(); } else { entity.Ref += 1; - await _database.SaveChangesAsync(); } + await _database.SaveChangesAsync(); return tag; } diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs index 5a07da96..52d079a3 100644 --- a/Timeline/Services/UserAvatarService.cs +++ b/Timeline/Services/UserAvatarService.cs @@ -258,6 +258,7 @@ namespace Timeline.Services else { await _avatarValidator.Validate(avatar); + var tag = await _dataManager.RetainEntry(avatar.Data); var oldTag = avatarEntity?.DataTag; var create = avatarEntity == null; if (avatarEntity == null) @@ -265,7 +266,6 @@ namespace Timeline.Services avatarEntity = new UserAvatarEntity(); _database.UserAvatars.Add(avatarEntity); } - var tag = await _dataManager.RetainEntry(avatar.Data); avatarEntity.DataTag = tag; avatarEntity.Type = avatar.Type; avatarEntity.LastModified = _clock.GetCurrentTime(); @@ -287,7 +287,7 @@ namespace Timeline.Services public static void AddUserAvatarService(this IServiceCollection services) { services.AddScoped(); - services.AddSingleton(); + services.AddScoped(); services.AddTransient(); } } diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 14bd14cc..263e6b7a 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -93,6 +93,10 @@ namespace Timeline services.AddScoped(); services.AddScoped(); services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + services.AddUserAvatarService(); services.AddScoped(); -- cgit v1.2.3