From 63eec85627bcd3c584865d47a237de44bcdb8b98 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Tue, 20 Aug 2019 23:41:36 +0800 Subject: Use etag for cache. --- .../Helpers/AssertionResponseExtensions.cs | 3 +- Timeline.Tests/IntegratedTests/UserAvatarTests.cs | 25 ++++- Timeline.Tests/UserAvatarServiceTest.cs | 123 +++++++++++++++++---- Timeline/Controllers/UserAvatarController.cs | 23 ++-- Timeline/Entities/UserAvatar.cs | 4 + Timeline/Models/Http/Common.cs | 6 + Timeline/Services/DatabaseCorruptedException.cs | 15 +++ Timeline/Services/ETagGenerator.cs | 33 ++++++ Timeline/Services/UserAvatarService.cs | 97 +++++++++++++--- 9 files changed, 281 insertions(+), 48 deletions(-) create mode 100644 Timeline/Services/DatabaseCorruptedException.cs create mode 100644 Timeline/Services/ETagGenerator.cs diff --git a/Timeline.Tests/Helpers/AssertionResponseExtensions.cs b/Timeline.Tests/Helpers/AssertionResponseExtensions.cs index 38617b92..e67a172a 100644 --- a/Timeline.Tests/Helpers/AssertionResponseExtensions.cs +++ b/Timeline.Tests/Helpers/AssertionResponseExtensions.cs @@ -23,7 +23,8 @@ namespace Timeline.Tests.Helpers string padding = new string('\t', context.Depth); var res = (HttpResponseMessage)value; - return $"{newline}{padding} Status Code: {res.StatusCode} ; Body: {res.Content.ReadAsStringAsync().Result} ;"; + var body = res.Content.ReadAsStringAsync().Result; + return $"{newline}{padding} Status Code: {res.StatusCode} ; Body: {body.Substring(0, Math.Min(body.Length, 20))} ;"; } } diff --git a/Timeline.Tests/IntegratedTests/UserAvatarTests.cs b/Timeline.Tests/IntegratedTests/UserAvatarTests.cs index 0bed9598..1cb15edb 100644 --- a/Timeline.Tests/IntegratedTests/UserAvatarTests.cs +++ b/Timeline.Tests/IntegratedTests/UserAvatarTests.cs @@ -63,7 +63,16 @@ namespace Timeline.Tests.IntegratedTests body.Should().Equal(defaultAvatarData); } - await GetReturnDefault(); + EntityTagHeaderValue eTag; + { + var res = await client.GetAsync($"users/user/avatar"); + res.Should().HaveStatusCodeOk(); + res.Content.Headers.ContentType.MediaType.Should().Be("image/png"); + var body = await res.Content.ReadAsByteArrayAsync(); + body.Should().Equal(defaultAvatarData); + eTag = res.Headers.ETag; + } + await GetReturnDefault("admin"); { @@ -72,7 +81,19 @@ namespace Timeline.Tests.IntegratedTests RequestUri = new Uri(client.BaseAddress, "users/user/avatar"), Method = HttpMethod.Get, }; - request.Headers.Add("If-Modified-Since", DateTime.Now.ToString("r")); + request.Headers.TryAddWithoutValidation("If-None-Match", "\"dsdfd"); + var res = await client.SendAsync(request); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.Should().HaveBodyAsCommonResponseWithCode(CommonResponse.ErrorCodes.Header_BadFormat_IfNonMatch); + } + + { + var request = new HttpRequestMessage() + { + RequestUri = new Uri(client.BaseAddress, "users/user/avatar"), + Method = HttpMethod.Get, + }; + request.Headers.Add ("If-None-Match", eTag.ToString()); var res = await client.SendAsync(request); res.Should().HaveStatusCode(HttpStatusCode.NotModified); } diff --git a/Timeline.Tests/UserAvatarServiceTest.cs b/Timeline.Tests/UserAvatarServiceTest.cs index f11da4f0..e059602d 100644 --- a/Timeline.Tests/UserAvatarServiceTest.cs +++ b/Timeline.Tests/UserAvatarServiceTest.cs @@ -17,12 +17,19 @@ namespace Timeline.Tests { public class MockDefaultUserAvatarProvider : IDefaultUserAvatarProvider { + public static string ETag { get; } = "Hahaha"; + public static AvatarInfo AvatarInfo { get; } = new AvatarInfo { Avatar = new Avatar { Type = "image/test", Data = Encoding.ASCII.GetBytes("test") }, LastModified = DateTime.Now }; + public Task GetDefaultAvatarETag() + { + return Task.FromResult(ETag); + } + public Task GetDefaultAvatar() { return Task.FromResult(AvatarInfo); @@ -100,23 +107,55 @@ namespace Timeline.Tests public class UserAvatarServiceTest : IDisposable, IClassFixture, IClassFixture { - private static Avatar MockAvatar { get; } = new Avatar + private UserAvatar MockAvatarEntity1 { get; } = new UserAvatar { Type = "image/testaaa", - Data = Encoding.ASCII.GetBytes("amock") + Data = Encoding.ASCII.GetBytes("amock"), + ETag = "aaaa", + LastModified = DateTime.Now }; - private static Avatar MockAvatar2 { get; } = new Avatar + private UserAvatar MockAvatarEntity2 { get; } = new UserAvatar { Type = "image/testbbb", - Data = Encoding.ASCII.GetBytes("bmock") + Data = Encoding.ASCII.GetBytes("bmock"), + ETag = "bbbb", + LastModified = DateTime.Now + TimeSpan.FromMinutes(1) }; + private Avatar ToAvatar(UserAvatar entity) + { + return new Avatar + { + Data = entity.Data, + Type = entity.Type + }; + } + + private AvatarInfo ToAvatarInfo(UserAvatar entity) + { + return new AvatarInfo + { + Avatar = ToAvatar(entity), + LastModified = entity.LastModified + }; + } + + private void Set(UserAvatar to, UserAvatar from) + { + to.Type = from.Type; + to.Data = from.Data; + to.ETag = from.ETag; + to.LastModified = from.LastModified; + } + private readonly MockDefaultUserAvatarProvider _mockDefaultUserAvatarProvider; private readonly LoggerFactory _loggerFactory; private readonly TestDatabase _database; + private readonly IETagGenerator _eTagGenerator; + private readonly UserAvatarService _service; public UserAvatarServiceTest(ITestOutputHelper outputHelper, MockDefaultUserAvatarProvider mockDefaultUserAvatarProvider, MockUserAvatarValidator mockUserAvatarValidator) @@ -126,7 +165,9 @@ namespace Timeline.Tests _loggerFactory = MyTestLoggerFactory.Create(outputHelper); _database = new TestDatabase(); - _service = new UserAvatarService(_loggerFactory.CreateLogger(), _database.DatabaseContext, _mockDefaultUserAvatarProvider, mockUserAvatarValidator); + _eTagGenerator = new ETagGenerator(); + + _service = new UserAvatarService(_loggerFactory.CreateLogger(), _database.DatabaseContext, _mockDefaultUserAvatarProvider, mockUserAvatarValidator, _eTagGenerator); } public void Dispose() @@ -135,6 +176,46 @@ namespace Timeline.Tests _database.Dispose(); } + [Fact] + public void GetAvatarETag_ShouldThrow_ArgumentException() + { + // no need to await because arguments are checked syncronizedly. + _service.Invoking(s => s.GetAvatarETag(null)).Should().Throw() + .Where(e => e.ParamName == "username" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase)); + _service.Invoking(s => s.GetAvatarETag("")).Should().Throw() + .Where(e => e.ParamName == "username" && e.Message.Contains("empty", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public void GetAvatarETag_ShouldThrow_UserNotExistException() + { + const string username = "usernotexist"; + _service.Awaiting(s => s.GetAvatarETag(username)).Should().Throw() + .Where(e => e.Username == username); + } + + [Fact] + public async Task GetAvatarETag_ShouldReturn_Default() + { + const string username = MockUsers.UserUsername; + (await _service.GetAvatarETag(username)).Should().BeEquivalentTo((await _mockDefaultUserAvatarProvider.GetDefaultAvatarETag())); + } + + [Fact] + public async Task GetAvatarETag_ShouldReturn_Data() + { + const string username = MockUsers.UserUsername; + { + // create mock data + var context = _database.DatabaseContext; + var user = await context.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync(); + Set(user.Avatar, MockAvatarEntity1); + await context.SaveChangesAsync(); + } + + (await _service.GetAvatarETag(username)).Should().BeEquivalentTo(MockAvatarEntity1.ETag); + } + [Fact] public void GetAvatar_ShouldThrow_ArgumentException() { @@ -169,24 +250,21 @@ namespace Timeline.Tests // create mock data var context = _database.DatabaseContext; var user = await context.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync(); - user.Avatar = new UserAvatar - { - Type = MockAvatar.Type, - Data = MockAvatar.Data - }; + Set(user.Avatar, MockAvatarEntity1); await context.SaveChangesAsync(); } - (await _service.GetAvatar(username)).Avatar.Should().BeEquivalentTo(MockAvatar); + (await _service.GetAvatar(username)).Should().BeEquivalentTo(ToAvatarInfo(MockAvatarEntity1)); } [Fact] public void SetAvatar_ShouldThrow_ArgumentException() { + var avatar = ToAvatar(MockAvatarEntity1); // no need to await because arguments are checked syncronizedly. - _service.Invoking(s => s.SetAvatar(null, MockAvatar)).Should().Throw() + _service.Invoking(s => s.SetAvatar(null, avatar)).Should().Throw() .Where(e => e.ParamName == "username" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase)); - _service.Invoking(s => s.SetAvatar("", MockAvatar)).Should().Throw() + _service.Invoking(s => s.SetAvatar("", avatar)).Should().Throw() .Where(e => e.ParamName == "username" && e.Message.Contains("empty", StringComparison.OrdinalIgnoreCase)); _service.Invoking(s => s.SetAvatar("aaa", new Avatar { Type = null, Data = new[] { (byte)0x00 } })).Should().Throw() @@ -202,7 +280,7 @@ namespace Timeline.Tests public void SetAvatar_ShouldThrow_UserNotExistException() { const string username = "usernotexist"; - _service.Awaiting(s => s.SetAvatar(username, MockAvatar)).Should().Throw() + _service.Awaiting(s => s.SetAvatar(username, ToAvatar(MockAvatarEntity1))).Should().Throw() .Where(e => e.Username == username); } @@ -214,21 +292,26 @@ namespace Timeline.Tests var user = await _database.DatabaseContext.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync(); // create - await _service.SetAvatar(username, MockAvatar); + var avatar1 = ToAvatar(MockAvatarEntity1); + await _service.SetAvatar(username, avatar1); user.Avatar.Should().NotBeNull(); - user.Avatar.Type.Should().Be(MockAvatar.Type); - user.Avatar.Data.Should().Equal(MockAvatar.Data); + user.Avatar.Type.Should().Be(avatar1.Type); + user.Avatar.Data.Should().Equal(avatar1.Data); + user.Avatar.ETag.Should().NotBeNull(); // modify - await _service.SetAvatar(username, MockAvatar2); + var avatar2 = ToAvatar(MockAvatarEntity2); + await _service.SetAvatar(username, avatar2); user.Avatar.Should().NotBeNull(); - user.Avatar.Type.Should().Be(MockAvatar2.Type); - user.Avatar.Data.Should().Equal(MockAvatar2.Data); + user.Avatar.Type.Should().Be(MockAvatarEntity2.Type); + user.Avatar.Data.Should().Equal(MockAvatarEntity2.Data); + user.Avatar.ETag.Should().NotBeNull(); // delete await _service.SetAvatar(username, null); user.Avatar.Type.Should().BeNull(); user.Avatar.Data.Should().BeNull(); + user.Avatar.ETag.Should().BeNull(); } } } diff --git a/Timeline/Controllers/UserAvatarController.cs b/Timeline/Controllers/UserAvatarController.cs index ffadcb86..964c9b98 100644 --- a/Timeline/Controllers/UserAvatarController.cs +++ b/Timeline/Controllers/UserAvatarController.cs @@ -2,7 +2,9 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; using System; +using System.Linq; using System.Threading.Tasks; using Timeline.Authenticate; using Timeline.Filters; @@ -61,22 +63,23 @@ namespace Timeline.Controllers [Authorize] public async Task Get([FromRoute] string username) { - const string IfModifiedSinceHeaderKey = "If-Modified-Since"; + const string IfNonMatchHeaderKey = "If-None-Match"; try { - var avatarInfo = await _service.GetAvatar(username); - var avatar = avatarInfo.Avatar; - if (Request.Headers.TryGetValue(IfModifiedSinceHeaderKey, out var value)) + var eTag = new EntityTagHeaderValue($"\"{await _service.GetAvatarETag(username)}\""); + + if (Request.Headers.TryGetValue(IfNonMatchHeaderKey, out var value)) { - var t = DateTime.Parse(value); - if (t > avatarInfo.LastModified) - { - Response.Headers.Add(IfModifiedSinceHeaderKey, avatarInfo.LastModified.ToString("r")); + if (!EntityTagHeaderValue.TryParseStrictList(value, out var eTagList)) + return BadRequest(CommonResponse.BadIfNonMatch()); + + if (eTagList.First(e => e.Equals(eTag)) != null) return StatusCode(StatusCodes.Status304NotModified); - } } - return File(avatar.Data, avatar.Type, new DateTimeOffset(avatarInfo.LastModified), null); + var avatarInfo = await _service.GetAvatar(username); + var avatar = avatarInfo.Avatar; + return File(avatar.Data, avatar.Type, new DateTimeOffset(avatarInfo.LastModified), eTag); } catch (UserNotExistException e) { diff --git a/Timeline/Entities/UserAvatar.cs b/Timeline/Entities/UserAvatar.cs index b941445d..d549aea5 100644 --- a/Timeline/Entities/UserAvatar.cs +++ b/Timeline/Entities/UserAvatar.cs @@ -16,6 +16,9 @@ namespace Timeline.Entities [Column("type")] public string Type { get; set; } + [Column("etag"), MaxLength(30)] + public string ETag { get; set; } + [Column("last_modified"), Required] public DateTime LastModified { get; set; } @@ -28,6 +31,7 @@ namespace Timeline.Entities Id = 0, Data = null, Type = null, + ETag = null, LastModified = lastModified }; } diff --git a/Timeline/Models/Http/Common.cs b/Timeline/Models/Http/Common.cs index 50f6836e..a72f187c 100644 --- a/Timeline/Models/Http/Common.cs +++ b/Timeline/Models/Http/Common.cs @@ -13,6 +13,7 @@ namespace Timeline.Models.Http public const int Header_Missing_ContentType = -111; public const int Header_Missing_ContentLength = -112; public const int Header_Zero_ContentLength = -113; + public const int Header_BadFormat_IfNonMatch = -114; } public static CommonResponse InvalidModel(string message) @@ -35,6 +36,11 @@ namespace Timeline.Models.Http return new CommonResponse(ErrorCodes.Header_Zero_ContentLength, "Header Content-Length must not be 0."); } + public static CommonResponse BadIfNonMatch() + { + return new CommonResponse(ErrorCodes.Header_BadFormat_IfNonMatch, "Header If-Non-Match is of bad format."); + } + public CommonResponse() { diff --git a/Timeline/Services/DatabaseCorruptedException.cs b/Timeline/Services/DatabaseCorruptedException.cs new file mode 100644 index 00000000..9988e0ad --- /dev/null +++ b/Timeline/Services/DatabaseCorruptedException.cs @@ -0,0 +1,15 @@ +using System; + +namespace Timeline.Services +{ + [Serializable] + public class DatabaseCorruptedException : Exception + { + public DatabaseCorruptedException() { } + public DatabaseCorruptedException(string message) : base(message) { } + public DatabaseCorruptedException(string message, Exception inner) : base(message, inner) { } + protected DatabaseCorruptedException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + } +} diff --git a/Timeline/Services/ETagGenerator.cs b/Timeline/Services/ETagGenerator.cs new file mode 100644 index 00000000..e2abebdc --- /dev/null +++ b/Timeline/Services/ETagGenerator.cs @@ -0,0 +1,33 @@ +using System; +using System.Security.Cryptography; + +namespace Timeline.Services +{ + public interface IETagGenerator + { + string Generate(byte[] source); + } + + public class ETagGenerator : IETagGenerator, IDisposable + { + private readonly SHA1 _sha1; + + public ETagGenerator() + { + _sha1 = SHA1.Create(); + } + + public string Generate(byte[] source) + { + if (source == null || source.Length == 0) + throw new ArgumentException("Source is null or empty.", nameof(source)); + + return Convert.ToBase64String(_sha1.ComputeHash(source)); + } + + public void Dispose() + { + _sha1.Dispose(); + } + } +} diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs index a83b8a52..7b1f405c 100644 --- a/Timeline/Services/UserAvatarService.cs +++ b/Timeline/Services/UserAvatarService.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats; @@ -64,6 +65,12 @@ namespace Timeline.Services /// public interface IDefaultUserAvatarProvider { + /// + /// Get the etag of default avatar. + /// + /// + Task GetDefaultAvatarETag(); + /// /// Get the default avatar. /// @@ -82,6 +89,15 @@ namespace Timeline.Services public interface IUserAvatarService { + /// + /// Get the etag of a user's avatar. + /// + /// The username of the user to get avatar etag of. + /// The etag. + /// Thrown if is null or empty. + /// Thrown if the user does not exist. + Task GetAvatarETag(string username); + /// /// Get avatar of a user. If the user has no avatar, a default one is returned. /// @@ -107,22 +123,46 @@ namespace Timeline.Services { private readonly IHostingEnvironment _environment; - public DefaultUserAvatarProvider(IHostingEnvironment environment) + private readonly IETagGenerator _eTagGenerator; + + private byte[] _cacheData; + private DateTime _cacheLastModified; + private string _cacheETag; + + public DefaultUserAvatarProvider(IHostingEnvironment environment, IETagGenerator eTagGenerator) { _environment = environment; + _eTagGenerator = eTagGenerator; } - public async Task GetDefaultAvatar() + private async Task CheckAndInit() { + if (_cacheData != null) + return; + var path = Path.Combine(_environment.ContentRootPath, "default-avatar.png"); + _cacheData = await File.ReadAllBytesAsync(path); + _cacheLastModified = File.GetLastWriteTime(path); + _cacheETag = _eTagGenerator.Generate(_cacheData); + } + + public async Task GetDefaultAvatarETag() + { + await CheckAndInit(); + return _cacheETag; + } + + public async Task GetDefaultAvatar() + { + await CheckAndInit(); return new AvatarInfo { Avatar = new Avatar { Type = "image/png", - Data = await File.ReadAllBytesAsync(path) + Data = _cacheData }, - LastModified = File.GetLastWriteTime(path) + LastModified = _cacheLastModified }; } } @@ -161,12 +201,36 @@ namespace Timeline.Services private readonly IDefaultUserAvatarProvider _defaultUserAvatarProvider; private readonly IUserAvatarValidator _avatarValidator; - public UserAvatarService(ILogger logger, DatabaseContext database, IDefaultUserAvatarProvider defaultUserAvatarProvider, IUserAvatarValidator avatarValidator) + private readonly IETagGenerator _eTagGenerator; + + public UserAvatarService( + ILogger logger, + DatabaseContext database, + IDefaultUserAvatarProvider defaultUserAvatarProvider, + IUserAvatarValidator avatarValidator, + IETagGenerator eTagGenerator) { _logger = logger; _database = database; _defaultUserAvatarProvider = defaultUserAvatarProvider; _avatarValidator = avatarValidator; + _eTagGenerator = eTagGenerator; + } + + public async Task GetAvatarETag(string username) + { + if (string.IsNullOrEmpty(username)) + throw new ArgumentException("Username is null or empty.", nameof(username)); + + var userId = await _database.Users.Where(u => u.Name == username).Select(u => u.Id).SingleOrDefaultAsync(); + if (userId == 0) + throw new UserNotExistException(username); + + var eTag = (await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.ETag }).SingleAsync()).ETag; + if (eTag == null) + return await _defaultUserAvatarProvider.GetDefaultAvatarETag(); + else + return eTag; } public async Task GetAvatar(string username) @@ -174,16 +238,17 @@ namespace Timeline.Services if (string.IsNullOrEmpty(username)) throw new ArgumentException("Username is null or empty.", nameof(username)); - var user = await _database.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); - if (user == null) + var userId = await _database.Users.Where(u => u.Name == username).Select(u => u.Id).SingleOrDefaultAsync(); + if (userId == 0) throw new UserNotExistException(username); - await _database.Entry(user).Reference(u => u.Avatar).LoadAsync(); - var avatar = user.Avatar; + var avatar = await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.Type, a.Data, a.LastModified }).SingleAsync(); - if ((avatar.Type == null) == (avatar.Data == null)) + if ((avatar.Type == null) != (avatar.Data == null)) + { _logger.LogCritical("Database corupted! One of type and data of a avatar is null but the other is not."); - // TODO: Throw an exception to indicate this. + throw new DatabaseCorruptedException(); + } if (avatar.Data == null) { @@ -218,12 +283,11 @@ namespace Timeline.Services throw new ArgumentException("Data of avatar is null.", nameof(avatar)); } - var user = await _database.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); - if (user == null) + var userId = await _database.Users.Where(u => u.Name == username).Select(u => u.Id).SingleOrDefaultAsync(); + if (userId == 0) throw new UserNotExistException(username); - await _database.Entry(user).Reference(u => u.Avatar).LoadAsync(); - var avatarEntity = user.Avatar; + var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleAsync(); if (avatar == null) { @@ -233,6 +297,7 @@ namespace Timeline.Services { avatarEntity.Data = null; avatarEntity.Type = null; + avatarEntity.ETag = null; avatarEntity.LastModified = DateTime.Now; await _database.SaveChangesAsync(); _logger.LogInformation("Updated an entry in user_avatars."); @@ -243,6 +308,7 @@ namespace Timeline.Services await _avatarValidator.Validate(avatar); avatarEntity.Type = avatar.Type; avatarEntity.Data = avatar.Data; + avatarEntity.ETag = _eTagGenerator.Generate(avatar.Data); avatarEntity.LastModified = DateTime.Now; await _database.SaveChangesAsync(); _logger.LogInformation("Updated an entry in user_avatars."); @@ -254,6 +320,7 @@ namespace Timeline.Services { public static void AddUserAvatarService(this IServiceCollection services) { + services.TryAddTransient(); services.AddScoped(); services.AddSingleton(); services.AddSingleton(); -- cgit v1.2.3 From f657105462b7a8c528b39005d81ffe6141f476a5 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Tue, 20 Aug 2019 23:54:22 +0800 Subject: Update Database. --- Timeline/Entities/DatabaseContext.cs | 2 +- .../20190820155221_AddAvatarETag.Designer.cs | 92 +++++++++++++++++++++ .../Migrations/20190820155221_AddAvatarETag.cs | 23 ++++++ ...90820155354_MakeUserNameIndexUnique.Designer.cs | 93 ++++++++++++++++++++++ .../20190820155354_MakeUserNameIndexUnique.cs | 32 ++++++++ .../Migrations/DatabaseContextModelSnapshot.cs | 7 +- 6 files changed, 247 insertions(+), 2 deletions(-) create mode 100644 Timeline/Migrations/20190820155221_AddAvatarETag.Designer.cs create mode 100644 Timeline/Migrations/20190820155221_AddAvatarETag.cs create mode 100644 Timeline/Migrations/20190820155354_MakeUserNameIndexUnique.Designer.cs create mode 100644 Timeline/Migrations/20190820155354_MakeUserNameIndexUnique.cs diff --git a/Timeline/Entities/DatabaseContext.cs b/Timeline/Entities/DatabaseContext.cs index bc06b9df..6e1fc638 100644 --- a/Timeline/Entities/DatabaseContext.cs +++ b/Timeline/Entities/DatabaseContext.cs @@ -43,7 +43,7 @@ namespace Timeline.Entities protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity().Property(e => e.Version).HasDefaultValue(0); - modelBuilder.Entity().HasIndex(e => e.Name); + modelBuilder.Entity().HasIndex(e => e.Name).IsUnique(); } public DbSet Users { get; set; } diff --git a/Timeline/Migrations/20190820155221_AddAvatarETag.Designer.cs b/Timeline/Migrations/20190820155221_AddAvatarETag.Designer.cs new file mode 100644 index 00000000..e7c7cb2f --- /dev/null +++ b/Timeline/Migrations/20190820155221_AddAvatarETag.Designer.cs @@ -0,0 +1,92 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Entities; + +namespace Timeline.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20190820155221_AddAvatarETag")] + partial class AddAvatarETag + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Timeline.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id"); + + b.Property("EncryptedPassword") + .IsRequired() + .HasColumnName("password"); + + b.Property("Name") + .IsRequired() + .HasColumnName("name") + .HasMaxLength(26); + + b.Property("RoleString") + .IsRequired() + .HasColumnName("roles"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnName("version") + .HasDefaultValue(0L); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatar", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id"); + + b.Property("Data") + .HasColumnName("data"); + + b.Property("ETag") + .HasColumnName("etag") + .HasMaxLength(30); + + b.Property("LastModified") + .HasColumnName("last_modified"); + + b.Property("Type") + .HasColumnName("type"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatar", b => + { + b.HasOne("Timeline.Entities.User") + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatar", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Timeline/Migrations/20190820155221_AddAvatarETag.cs b/Timeline/Migrations/20190820155221_AddAvatarETag.cs new file mode 100644 index 00000000..db352b5d --- /dev/null +++ b/Timeline/Migrations/20190820155221_AddAvatarETag.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Timeline.Migrations +{ + public partial class AddAvatarETag : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "etag", + table: "user_avatars", + maxLength: 30, + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "etag", + table: "user_avatars"); + } + } +} diff --git a/Timeline/Migrations/20190820155354_MakeUserNameIndexUnique.Designer.cs b/Timeline/Migrations/20190820155354_MakeUserNameIndexUnique.Designer.cs new file mode 100644 index 00000000..420cd41c --- /dev/null +++ b/Timeline/Migrations/20190820155354_MakeUserNameIndexUnique.Designer.cs @@ -0,0 +1,93 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Entities; + +namespace Timeline.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20190820155354_MakeUserNameIndexUnique")] + partial class MakeUserNameIndexUnique + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Timeline.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id"); + + b.Property("EncryptedPassword") + .IsRequired() + .HasColumnName("password"); + + b.Property("Name") + .IsRequired() + .HasColumnName("name") + .HasMaxLength(26); + + b.Property("RoleString") + .IsRequired() + .HasColumnName("roles"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnName("version") + .HasDefaultValue(0L); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatar", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id"); + + b.Property("Data") + .HasColumnName("data"); + + b.Property("ETag") + .HasColumnName("etag") + .HasMaxLength(30); + + b.Property("LastModified") + .HasColumnName("last_modified"); + + b.Property("Type") + .HasColumnName("type"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatar", b => + { + b.HasOne("Timeline.Entities.User") + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatar", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Timeline/Migrations/20190820155354_MakeUserNameIndexUnique.cs b/Timeline/Migrations/20190820155354_MakeUserNameIndexUnique.cs new file mode 100644 index 00000000..01d72450 --- /dev/null +++ b/Timeline/Migrations/20190820155354_MakeUserNameIndexUnique.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Timeline.Migrations +{ + public partial class MakeUserNameIndexUnique : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_users_name", + table: "users"); + + migrationBuilder.CreateIndex( + name: "IX_users_name", + table: "users", + column: "name", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_users_name", + table: "users"); + + migrationBuilder.CreateIndex( + name: "IX_users_name", + table: "users", + column: "name"); + } + } +} diff --git a/Timeline/Migrations/DatabaseContextModelSnapshot.cs b/Timeline/Migrations/DatabaseContextModelSnapshot.cs index 0eb85997..4941321c 100644 --- a/Timeline/Migrations/DatabaseContextModelSnapshot.cs +++ b/Timeline/Migrations/DatabaseContextModelSnapshot.cs @@ -43,7 +43,8 @@ namespace Timeline.Migrations b.HasKey("Id"); - b.HasIndex("Name"); + b.HasIndex("Name") + .IsUnique(); b.ToTable("users"); }); @@ -57,6 +58,10 @@ namespace Timeline.Migrations b.Property("Data") .HasColumnName("data"); + b.Property("ETag") + .HasColumnName("etag") + .HasMaxLength(30); + b.Property("LastModified") .HasColumnName("last_modified"); -- cgit v1.2.3