From 8a1ecbf49673cb2bed538ac8bc4e82691b90d973 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 13 Mar 2020 17:05:03 +0800 Subject: Abstract out data cache helper. --- Timeline/Controllers/UserAvatarController.cs | 31 ++++------------------------ 1 file changed, 4 insertions(+), 27 deletions(-) (limited to 'Timeline/Controllers/UserAvatarController.cs') diff --git a/Timeline/Controllers/UserAvatarController.cs b/Timeline/Controllers/UserAvatarController.cs index f4f3db3e..f78dcb08 100644 --- a/Timeline/Controllers/UserAvatarController.cs +++ b/Timeline/Controllers/UserAvatarController.cs @@ -46,34 +46,11 @@ namespace Timeline.Controllers return NotFound(ErrorResponse.UserCommon.NotExist()); } - const string IfNonMatchHeaderKey = "If-None-Match"; - - var eTagValue = $"\"{await _service.GetAvatarETag(id)}\""; - var eTag = new EntityTagHeaderValue(eTagValue); - - if (Request.Headers.TryGetValue(IfNonMatchHeaderKey, out var value)) + return await DataCacheHelper.GenerateActionResult(this, () => _service.GetAvatarETag(id), async () => { - if (!EntityTagHeaderValue.TryParseStrictList(value, out var eTagList)) - { - _logger.LogInformation(Log.Format(LogGetBadIfNoneMatch, - ("Username", username), ("If-None-Match", value))); - return BadRequest(ErrorResponse.Common.Header.IfNonMatch_BadFormat()); - } - - if (eTagList.FirstOrDefault(e => e.Equals(eTag)) != null) - { - Response.Headers.Add("ETag", eTagValue); - _logger.LogInformation(Log.Format(LogGetReturnNotModify, ("Username", username))); - return StatusCode(StatusCodes.Status304NotModified); - } - } - - var avatarInfo = await _service.GetAvatar(id); - var avatar = avatarInfo.Avatar; - - _logger.LogInformation(Log.Format(LogGetReturnData, ("Username", username))); - return File(avatar.Data, avatar.Type, new DateTimeOffset(avatarInfo.LastModified), eTag); - + var avatar = await _service.GetAvatar(id); + return avatar.ToCacheableData(); + }); } [HttpPut("users/{username}/avatar")] -- cgit v1.2.3 From 636cf3839d92e884987e4e3aec7f23953d02fe37 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 13 Mar 2020 17:58:32 +0800 Subject: Add cache for timeline post data. --- Timeline.Tests/Helpers/CacheTestHelper.cs | 64 ++++++++++++++++++++++++ Timeline.Tests/IntegratedTests/TimelineTest.cs | 4 ++ Timeline.Tests/IntegratedTests/UserAvatarTest.cs | 45 ++--------------- Timeline/Controllers/TimelineController.cs | 9 ++-- Timeline/Controllers/UserAvatarController.cs | 1 - Timeline/Helpers/DataCacheHelper.cs | 25 +++++++-- Timeline/Services/TimelineService.cs | 49 +++++++++++++++++- 7 files changed, 145 insertions(+), 52 deletions(-) create mode 100644 Timeline.Tests/Helpers/CacheTestHelper.cs (limited to 'Timeline/Controllers/UserAvatarController.cs') diff --git a/Timeline.Tests/Helpers/CacheTestHelper.cs b/Timeline.Tests/Helpers/CacheTestHelper.cs new file mode 100644 index 00000000..b3709a28 --- /dev/null +++ b/Timeline.Tests/Helpers/CacheTestHelper.cs @@ -0,0 +1,64 @@ +using FluentAssertions; +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Timeline.Models.Http; + +namespace Timeline.Tests.Helpers +{ + public static class CacheTestHelper + { + public static async Task TestCache(HttpClient client, string getUrl) + { + EntityTagHeaderValue eTag; + { + var res = await client.GetAsync(getUrl); + res.Should().HaveStatusCode(200); + var cacheControlHeader = res.Headers.CacheControl; + cacheControlHeader.NoCache.Should().BeTrue(); + cacheControlHeader.NoStore.Should().BeFalse(); + cacheControlHeader.Private.Should().BeTrue(); + cacheControlHeader.Public.Should().BeFalse(); + cacheControlHeader.MustRevalidate.Should().BeTrue(); + cacheControlHeader.MaxAge.Should().NotBeNull().And.Be(TimeSpan.FromDays(14)); + eTag = res.Headers.ETag; + } + + { + using var request = new HttpRequestMessage() + { + RequestUri = new Uri(client.BaseAddress, getUrl), + Method = HttpMethod.Get, + }; + request.Headers.TryAddWithoutValidation("If-None-Match", "\"dsdfd"); + var res = await client.SendAsync(request); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.HaveCommonBody(ErrorCodes.Common.Header.IfNonMatch_BadFormat); + } + + { + using var request = new HttpRequestMessage() + { + RequestUri = new Uri(client.BaseAddress, getUrl), + Method = HttpMethod.Get, + }; + request.Headers.TryAddWithoutValidation("If-None-Match", "\"aaa\""); + var res = await client.SendAsync(request); + res.Should().HaveStatusCode(HttpStatusCode.OK); + } + + { + using var request = new HttpRequestMessage() + { + RequestUri = new Uri(client.BaseAddress, getUrl), + 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/IntegratedTests/TimelineTest.cs b/Timeline.Tests/IntegratedTests/TimelineTest.cs index 682cfd7c..845208e8 100644 --- a/Timeline.Tests/IntegratedTests/TimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/TimelineTest.cs @@ -1090,6 +1090,10 @@ namespace Timeline.Tests.IntegratedTests format.Name.Should().Be(PngFormat.Instance.Name); } + { + await CacheTestHelper.TestCache(client, generator(1, $"posts/{postId}/data")); + } + { var res = await client.DeleteAsync(generator(1, $"posts/{postId}")); res.Should().BeDelete(true); diff --git a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs index fa0120f1..fbf34490 100644 --- a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs +++ b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs @@ -6,7 +6,6 @@ using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Png; -using System; using System.Collections.Generic; using System.IO; using System.Net; @@ -58,55 +57,17 @@ namespace Timeline.Tests.IntegratedTests body.Should().Equal(defaultAvatarData); } - EntityTagHeaderValue eTag; { - var res = await client.GetAsync($"users/user1/avatar"); + var res = await client.GetAsync("users/user1/avatar"); res.Should().HaveStatusCode(200); res.Content.Headers.ContentType.MediaType.Should().Be("image/png"); var body = await res.Content.ReadAsByteArrayAsync(); body.Should().Equal(defaultAvatarData); - var cacheControl = res.Headers.CacheControl; - cacheControl.NoCache.Should().BeTrue(); - cacheControl.NoStore.Should().BeFalse(); - cacheControl.MaxAge.Should().NotBeNull().And.Be(TimeSpan.Zero); - eTag = res.Headers.ETag; } - await GetReturnDefault("admin"); - - { - using var request = new HttpRequestMessage() - { - RequestUri = new Uri(client.BaseAddress, "users/user1/avatar"), - Method = HttpMethod.Get, - }; - request.Headers.TryAddWithoutValidation("If-None-Match", "\"dsdfd"); - var res = await client.SendAsync(request); - res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Header.IfNonMatch_BadFormat); - } - - { - using var request = new HttpRequestMessage() - { - RequestUri = new Uri(client.BaseAddress, "users/user1/avatar"), - Method = HttpMethod.Get, - }; - request.Headers.TryAddWithoutValidation("If-None-Match", "\"aaa\""); - var res = await client.SendAsync(request); - res.Should().HaveStatusCode(HttpStatusCode.OK); - } + await CacheTestHelper.TestCache(client, "users/user1/avatar"); - { - using var request = new HttpRequestMessage() - { - RequestUri = new Uri(client.BaseAddress, "users/user1/avatar"), - Method = HttpMethod.Get, - }; - request.Headers.Add("If-None-Match", eTag.ToString()); - var res = await client.SendAsync(request); - res.Should().HaveStatusCode(HttpStatusCode.NotModified); - } + await GetReturnDefault("admin"); { using var content = new ByteArrayContent(new[] { (byte)0x00 }); diff --git a/Timeline/Controllers/TimelineController.cs b/Timeline/Controllers/TimelineController.cs index 58390c29..8bc0345f 100644 --- a/Timeline/Controllers/TimelineController.cs +++ b/Timeline/Controllers/TimelineController.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Timeline.Filters; +using Timeline.Helpers; using Timeline.Models; using Timeline.Models.Http; using Timeline.Models.Validation; @@ -114,7 +115,6 @@ namespace Timeline.Controllers return result; } - // TODO: Make cache available. [HttpGet("timelines/{name}/posts/{id}/data")] public async Task>> PostDataGet([FromRoute][GeneralTimelineName] string name, [FromRoute] long id) { @@ -125,8 +125,11 @@ namespace Timeline.Controllers try { - var data = await _service.GetPostData(name, id); - return File(data.Data, data.Type, data.LastModified, new EntityTagHeaderValue($"\"{data.ETag}\"")); + return await DataCacheHelper.GenerateActionResult(this, () => _service.GetPostDataETag(name, id), async () => + { + var data = await _service.GetPostData(name, id); + return data; + }); } catch (TimelinePostNotExistException) { diff --git a/Timeline/Controllers/UserAvatarController.cs b/Timeline/Controllers/UserAvatarController.cs index f78dcb08..b5f4be1e 100644 --- a/Timeline/Controllers/UserAvatarController.cs +++ b/Timeline/Controllers/UserAvatarController.cs @@ -32,7 +32,6 @@ namespace Timeline.Controllers } [HttpGet("users/{username}/avatar")] - [ResponseCache(NoStore = false, Location = ResponseCacheLocation.None, Duration = 0)] public async Task Get([FromRoute][Username] string username) { long id; diff --git a/Timeline/Helpers/DataCacheHelper.cs b/Timeline/Helpers/DataCacheHelper.cs index c13aaddb..574d90b4 100644 --- a/Timeline/Helpers/DataCacheHelper.cs +++ b/Timeline/Helpers/DataCacheHelper.cs @@ -66,11 +66,25 @@ namespace Timeline.Helpers public static class DataCacheHelper { - public static async Task GenerateActionResult(Controller controller, ICacheableDataProvider provider) + public static async Task GenerateActionResult(Controller controller, ICacheableDataProvider provider, TimeSpan? maxAge = null) { + const string CacheControlHeaderKey = "Cache-Control"; const string IfNonMatchHeaderKey = "If-None-Match"; const string ETagHeaderKey = "ETag"; + string GenerateCacheControlHeaderValue() + { + var cacheControlHeader = new CacheControlHeaderValue() + { + NoCache = true, + NoStore = false, + MaxAge = maxAge ?? TimeSpan.FromDays(14), + Private = true, + MustRevalidate = true + }; + return cacheControlHeader.ToString(); + } + var loggerFactory = controller.HttpContext.RequestServices.GetRequiredService(); var logger = loggerFactory.CreateLogger(typeof(DataCacheHelper)); @@ -89,20 +103,23 @@ namespace Timeline.Helpers if (eTagList.FirstOrDefault(e => e.Equals(eTag)) != null) { - controller.Response.Headers.Add(ETagHeaderKey, eTagValue); logger.LogInformation(LogResultNotModified); + controller.Response.Headers.Add(ETagHeaderKey, eTagValue); + controller.Response.Headers.Add(CacheControlHeaderKey, GenerateCacheControlHeaderValue()); + return controller.StatusCode(StatusCodes.Status304NotModified); } } var data = await provider.GetData(); logger.LogInformation(LogResultData); + controller.Response.Headers.Add(CacheControlHeaderKey, GenerateCacheControlHeaderValue()); return controller.File(data.Data, data.Type, data.LastModified, eTag); } - public static Task GenerateActionResult(Controller controller, Func> getDataETagDelegate, Func> getDataDelegate) + public static Task GenerateActionResult(Controller controller, Func> getDataETagDelegate, Func> getDataDelegate, TimeSpan? maxAge = null) { - return GenerateActionResult(controller, new DelegateCacheableDataProvider(getDataETagDelegate, getDataDelegate)); + return GenerateActionResult(controller, new DelegateCacheableDataProvider(getDataETagDelegate, getDataDelegate), maxAge); } } } diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 301a1d97..b26016e5 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; +using Timeline.Helpers; using Timeline.Models; using Timeline.Models.Validation; using static Timeline.Resources.Services.TimelineService; @@ -32,14 +33,14 @@ namespace Timeline.Services public long UserId { get; set; } } - public class PostData + public class PostData : ICacheableData { #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 string Type { get; set; } = default!; public string ETag { get; set; } = default!; - public DateTime LastModified { get; set; } = default!; + public DateTime? LastModified { get; set; } } /// @@ -91,6 +92,20 @@ namespace Timeline.Services /// See remarks of . Task> GetPosts(string name); + /// + /// Get the etag of data of a post. + /// + /// See remarks of . + /// The id of the post. + /// The etag of the data. + /// Thrown when is null. + /// See remarks of . + /// See remarks of . + /// Thrown when post of does not exist or has been deleted. + /// Thrown when post has no data. See remarks. + /// + Task GetPostDataETag(string name, long postId); + /// /// Get the data of a post. /// @@ -105,6 +120,7 @@ namespace Timeline.Services /// /// Use this method to retrieve the image of image post. /// + /// Task GetPostData(string name, long postId); /// @@ -402,6 +418,29 @@ namespace Timeline.Services } return posts; } + + public async Task GetPostDataETag(string name, long postId) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + var timelineId = await FindTimelineId(name); + var postEntity = await Database.TimelinePosts.Where(p => p.LocalId == postId).SingleOrDefaultAsync(); + + if (postEntity == null) + throw new TimelinePostNotExistException(name, postId); + + if (postEntity.Content == null) + throw new TimelinePostNotExistException(name, postId, true); + + if (postEntity.ContentType != TimelinePostContentTypes.Image) + throw new InvalidOperationException(ExceptionGetDataNonImagePost); + + var tag = postEntity.Content; + + return tag; + } + public async Task GetPostData(string name, long postId) { if (name == null) @@ -1014,6 +1053,12 @@ namespace Timeline.Services return s.GetPosts(realName); } + public Task GetPostDataETag(string name, long postId) + { + var s = BranchName(name, out var realName); + return s.GetPostDataETag(realName, postId); + } + public Task GetPostData(string name, long postId) { var s = BranchName(name, out var realName); -- cgit v1.2.3 From 5edffd0242a16e9866a1f4e9f1e1d2ff4e549d2c Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 13 Mar 2020 18:03:29 +0800 Subject: Clean code. --- Timeline.Tests/PasswordGenerator.cs | 3 +-- Timeline/Controllers/TimelineController.cs | 1 - Timeline/Controllers/UserAvatarController.cs | 2 -- Timeline/Helpers/Log.cs | 1 - Timeline/Models/Http/UserController.cs | 1 - Timeline/Models/Validation/Validator.cs | 5 +---- Timeline/Services/Clock.cs | 3 --- Timeline/Services/PathProvider.cs | 4 ---- Timeline/Startup.cs | 2 +- 9 files changed, 3 insertions(+), 19 deletions(-) (limited to 'Timeline/Controllers/UserAvatarController.cs') diff --git a/Timeline.Tests/PasswordGenerator.cs b/Timeline.Tests/PasswordGenerator.cs index 6c07836b..863439b5 100644 --- a/Timeline.Tests/PasswordGenerator.cs +++ b/Timeline.Tests/PasswordGenerator.cs @@ -1,5 +1,4 @@ -using System; -using Timeline.Services; +using Timeline.Services; using Xunit; using Xunit.Abstractions; diff --git a/Timeline/Controllers/TimelineController.cs b/Timeline/Controllers/TimelineController.cs index 8bc0345f..f1781ff3 100644 --- a/Timeline/Controllers/TimelineController.cs +++ b/Timeline/Controllers/TimelineController.cs @@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; diff --git a/Timeline/Controllers/UserAvatarController.cs b/Timeline/Controllers/UserAvatarController.cs index b5f4be1e..4062837b 100644 --- a/Timeline/Controllers/UserAvatarController.cs +++ b/Timeline/Controllers/UserAvatarController.cs @@ -2,9 +2,7 @@ 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.Auth; using Timeline.Filters; diff --git a/Timeline/Helpers/Log.cs b/Timeline/Helpers/Log.cs index 68c975fa..af0b7e13 100644 --- a/Timeline/Helpers/Log.cs +++ b/Timeline/Helpers/Log.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Text; namespace Timeline.Helpers diff --git a/Timeline/Models/Http/UserController.cs b/Timeline/Models/Http/UserController.cs index e4c95cbd..5ee02a95 100644 --- a/Timeline/Models/Http/UserController.cs +++ b/Timeline/Models/Http/UserController.cs @@ -1,7 +1,6 @@ using AutoMapper; using System.ComponentModel.DataAnnotations; using Timeline.Models.Validation; -using Timeline.Services; namespace Timeline.Models.Http { diff --git a/Timeline/Models/Validation/Validator.cs b/Timeline/Models/Validation/Validator.cs index ead7dbef..db139448 100644 --- a/Timeline/Models/Validation/Validator.cs +++ b/Timeline/Models/Validation/Validator.cs @@ -1,8 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Localization; -using System; +using System; using System.ComponentModel.DataAnnotations; -using Timeline.Helpers; using static Timeline.Resources.Models.Validation.Validator; namespace Timeline.Models.Validation diff --git a/Timeline/Services/Clock.cs b/Timeline/Services/Clock.cs index 0499c0c6..040f9304 100644 --- a/Timeline/Services/Clock.cs +++ b/Timeline/Services/Clock.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace Timeline.Services { diff --git a/Timeline/Services/PathProvider.cs b/Timeline/Services/PathProvider.cs index 15e66972..1f6b629a 100644 --- a/Timeline/Services/PathProvider.cs +++ b/Timeline/Services/PathProvider.cs @@ -1,9 +1,5 @@ using Microsoft.Extensions.Configuration; -using System; -using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Threading.Tasks; namespace Timeline.Services { diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 38bb3164..f5220446 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -108,7 +108,7 @@ namespace Timeline services.TryAddSingleton(); - services.AddDbContext((services, options )=> + services.AddDbContext((services, options) => { var pathProvider = services.GetRequiredService(); options.UseSqlite($"Data Source={pathProvider.GetDatabaseFilePath()}"); -- cgit v1.2.3