diff options
author | crupest <crupest@outlook.com> | 2020-03-13 18:09:50 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-03-13 18:09:50 +0800 |
commit | 067ef1edcb4409feda25a9356d49b146a9fd884b (patch) | |
tree | 23167ac72ad2a0a878eacbfec7fdd9b8b4a81c55 | |
parent | 904f98bda60b3bd92331aacde3771dedde62d2b5 (diff) | |
parent | 1af4f51abcd09be68bc0e2ad77b163569a50a6b3 (diff) | |
download | timeline-067ef1edcb4409feda25a9356d49b146a9fd884b.tar.gz timeline-067ef1edcb4409feda25a9356d49b146a9fd884b.tar.bz2 timeline-067ef1edcb4409feda25a9356d49b146a9fd884b.zip |
Merge pull request #71 from crupest/cache
Add cache support for timeline post data.
20 files changed, 489 insertions, 130 deletions
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 @@ -1091,6 +1091,10 @@ namespace Timeline.Tests.IntegratedTests }
{
+ 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.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 58390c29..f1781ff3 100644 --- a/Timeline/Controllers/TimelineController.cs +++ b/Timeline/Controllers/TimelineController.cs @@ -3,12 +3,12 @@ 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;
using System.Threading.Tasks;
using Timeline.Filters;
+using Timeline.Helpers;
using Timeline.Models;
using Timeline.Models.Http;
using Timeline.Models.Validation;
@@ -114,7 +114,6 @@ namespace Timeline.Controllers return result;
}
- // TODO: Make cache available.
[HttpGet("timelines/{name}/posts/{id}/data")]
public async Task<ActionResult<List<TimelinePostInfo>>> PostDataGet([FromRoute][GeneralTimelineName] string name, [FromRoute] long id)
{
@@ -125,8 +124,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 f4f3db3e..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;
@@ -32,7 +30,6 @@ namespace Timeline.Controllers }
[HttpGet("users/{username}/avatar")]
- [ResponseCache(NoStore = false, Location = ResponseCacheLocation.None, Duration = 0)]
public async Task<IActionResult> Get([FromRoute][Username] string username)
{
long id;
@@ -46,34 +43,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")]
diff --git a/Timeline/Helpers/DataCacheHelper.cs b/Timeline/Helpers/DataCacheHelper.cs new file mode 100644 index 00000000..574d90b4 --- /dev/null +++ b/Timeline/Helpers/DataCacheHelper.cs @@ -0,0 +1,125 @@ +using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Timeline.Models.Http;
+using static Timeline.Resources.Helper.DataCacheHelper;
+
+namespace Timeline.Helpers
+{
+ public interface ICacheableData
+ {
+ string Type { get; }
+#pragma warning disable CA1819 // Properties should not return arrays
+ byte[] Data { get; }
+#pragma warning restore CA1819 // Properties should not return arrays
+ DateTime? LastModified { get; }
+ }
+
+ public class CacheableData : ICacheableData
+ {
+ public CacheableData(string type, byte[] data, DateTime? lastModified)
+ {
+ Type = type;
+ Data = data;
+ LastModified = lastModified;
+ }
+
+ public string Type { get; set; }
+#pragma warning disable CA1819 // Properties should not return arrays
+ public byte[] Data { get; set; }
+#pragma warning restore CA1819 // Properties should not return arrays
+ public DateTime? LastModified { get; set; }
+ }
+
+ public interface ICacheableDataProvider
+ {
+ Task<string> GetDataETag();
+ Task<ICacheableData> GetData();
+ }
+
+ public class DelegateCacheableDataProvider : ICacheableDataProvider
+ {
+ private readonly Func<Task<string>> _getDataETagDelegate;
+ private readonly Func<Task<ICacheableData>> _getDataDelegate;
+
+ public DelegateCacheableDataProvider(Func<Task<string>> getDataETagDelegate, Func<Task<ICacheableData>> getDataDelegate)
+ {
+ _getDataETagDelegate = getDataETagDelegate;
+ _getDataDelegate = getDataDelegate;
+ }
+
+ public Task<ICacheableData> GetData()
+ {
+ return _getDataDelegate();
+ }
+
+ public Task<string> GetDataETag()
+ {
+ return _getDataETagDelegate();
+ }
+ }
+
+ public static class DataCacheHelper
+ {
+ public static async Task<ActionResult> 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<ILoggerFactory>();
+ var logger = loggerFactory.CreateLogger(typeof(DataCacheHelper));
+
+ var eTagValue = await provider.GetDataETag();
+ eTagValue = '"' + eTagValue + '"';
+ var eTag = new EntityTagHeaderValue(eTagValue);
+
+
+ if (controller.Request.Headers.TryGetValue(IfNonMatchHeaderKey, out var value))
+ {
+ if (!EntityTagHeaderValue.TryParseStrictList(value, out var eTagList))
+ {
+ logger.LogInformation(Log.Format(LogBadIfNoneMatch, ("Header Value", value)));
+ return controller.BadRequest(ErrorResponse.Common.Header.IfNonMatch_BadFormat());
+ }
+
+ if (eTagList.FirstOrDefault(e => e.Equals(eTag)) != null)
+ {
+ 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<ActionResult> GenerateActionResult(Controller controller, Func<Task<string>> getDataETagDelegate, Func<Task<ICacheableData>> getDataDelegate, TimeSpan? maxAge = null)
+ {
+ return GenerateActionResult(controller, new DelegateCacheableDataProvider(getDataETagDelegate, getDataDelegate), maxAge);
+ }
+ }
+}
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/Resources/Controllers/UserAvatarController.Designer.cs b/Timeline/Resources/Controllers/UserAvatarController.Designer.cs index e6eeb1e8..b0c35ff9 100644 --- a/Timeline/Resources/Controllers/UserAvatarController.Designer.cs +++ b/Timeline/Resources/Controllers/UserAvatarController.Designer.cs @@ -97,33 +97,6 @@ namespace Timeline.Resources.Controllers { }
/// <summary>
- /// Looks up a localized string similar to Attempt to get a avatar with If-None-Match in bad format..
- /// </summary>
- internal static string LogGetBadIfNoneMatch {
- get {
- return ResourceManager.GetString("LogGetBadIfNoneMatch", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to Returned full data for a get avatar attempt..
- /// </summary>
- internal static string LogGetReturnData {
- get {
- return ResourceManager.GetString("LogGetReturnData", resourceCulture);
- }
- }
-
- /// <summary>
- /// Looks up a localized string similar to Returned NotModify for a get avatar attempt..
- /// </summary>
- internal static string LogGetReturnNotModify {
- get {
- return ResourceManager.GetString("LogGetReturnNotModify", resourceCulture);
- }
- }
-
- /// <summary>
/// Looks up a localized string similar to Attempt to get a avatar of a non-existent user failed..
/// </summary>
internal static string LogGetUserNotExist {
diff --git a/Timeline/Resources/Controllers/UserAvatarController.resx b/Timeline/Resources/Controllers/UserAvatarController.resx index 58860c83..864d96c0 100644 --- a/Timeline/Resources/Controllers/UserAvatarController.resx +++ b/Timeline/Resources/Controllers/UserAvatarController.resx @@ -129,15 +129,6 @@ <data name="LogDeleteSuccess" xml:space="preserve">
<value>Succeed to delete a avatar of a user.</value>
</data>
- <data name="LogGetBadIfNoneMatch" xml:space="preserve">
- <value>Attempt to get a avatar with If-None-Match in bad format.</value>
- </data>
- <data name="LogGetReturnData" xml:space="preserve">
- <value>Returned full data for a get avatar attempt.</value>
- </data>
- <data name="LogGetReturnNotModify" xml:space="preserve">
- <value>Returned NotModify for a get avatar attempt.</value>
- </data>
<data name="LogGetUserNotExist" xml:space="preserve">
<value>Attempt to get a avatar of a non-existent user failed.</value>
</data>
diff --git a/Timeline/Resources/Helper/DataCacheHelper.Designer.cs b/Timeline/Resources/Helper/DataCacheHelper.Designer.cs new file mode 100644 index 00000000..acf56d13 --- /dev/null +++ b/Timeline/Resources/Helper/DataCacheHelper.Designer.cs @@ -0,0 +1,90 @@ +//------------------------------------------------------------------------------
+// <auto-generated>
+// 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.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Timeline.Resources.Helper {
+ using System;
+
+
+ /// <summary>
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ /// </summary>
+ // 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 DataCacheHelper {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal DataCacheHelper() {
+ }
+
+ /// <summary>
+ /// Returns the cached ResourceManager instance used by this class.
+ /// </summary>
+ [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.Helper.DataCacheHelper", typeof(DataCacheHelper).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ /// <summary>
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ /// </summary>
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Header If-None-Match is of bad format..
+ /// </summary>
+ internal static string LogBadIfNoneMatch {
+ get {
+ return ResourceManager.GetString("LogBadIfNoneMatch", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cache is invalid and data is returned..
+ /// </summary>
+ internal static string LogResultData {
+ get {
+ return ResourceManager.GetString("LogResultData", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Cache is valid and 304 Not Modified is returned..
+ /// </summary>
+ internal static string LogResultNotModified {
+ get {
+ return ResourceManager.GetString("LogResultNotModified", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/Timeline/Resources/Helper/DataCacheHelper.resx b/Timeline/Resources/Helper/DataCacheHelper.resx new file mode 100644 index 00000000..515cfa9b --- /dev/null +++ b/Timeline/Resources/Helper/DataCacheHelper.resx @@ -0,0 +1,129 @@ +<?xml version="1.0" encoding="utf-8"?>
+<root>
+ <!--
+ Microsoft ResX Schema
+
+ Version 2.0
+
+ The primary goals of this format is to allow a simple XML format
+ that is mostly human readable. The generation and parsing of the
+ various data types are done through the TypeConverter classes
+ associated with the data types.
+
+ Example:
+
+ ... ado.net/XML headers & schema ...
+ <resheader name="resmimetype">text/microsoft-resx</resheader>
+ <resheader name="version">2.0</resheader>
+ <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+ <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+ <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+ <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+ <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+ <value>[base64 mime encoded serialized .NET Framework object]</value>
+ </data>
+ <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+ <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+ <comment>This is a comment</comment>
+ </data>
+
+ There are any number of "resheader" rows that contain simple
+ name/value pairs.
+
+ Each data row contains a name, and value. The row also contains a
+ type or mimetype. Type corresponds to a .NET class that support
+ text/value conversion through the TypeConverter architecture.
+ Classes that don't support this are serialized and stored with the
+ mimetype set.
+
+ The mimetype is used for serialized objects, and tells the
+ ResXResourceReader how to depersist the object. This is currently not
+ extensible. For a given mimetype the value must be set accordingly:
+
+ Note - application/x-microsoft.net.object.binary.base64 is the format
+ that the ResXResourceWriter will generate, however the reader can
+ read any of the formats listed below.
+
+ mimetype: application/x-microsoft.net.object.binary.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.soap.base64
+ value : The object must be serialized with
+ : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+ : and then encoded with base64 encoding.
+
+ mimetype: application/x-microsoft.net.object.bytearray.base64
+ value : The object must be serialized into a byte array
+ : using a System.ComponentModel.TypeConverter
+ : and then encoded with base64 encoding.
+ -->
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <data name="LogBadIfNoneMatch" xml:space="preserve">
+ <value>Header If-None-Match is of bad format.</value>
+ </data>
+ <data name="LogResultData" xml:space="preserve">
+ <value>Cache is invalid and data is returned.</value>
+ </data>
+ <data name="LogResultNotModified" xml:space="preserve">
+ <value>Cache is valid and 304 Not Modified is returned.</value>
+ </data>
+</root>
\ No newline at end of file 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/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; }
}
/// <summary>
@@ -92,6 +93,20 @@ namespace Timeline.Services Task<List<TimelinePost>> GetPosts(string name);
/// <summary>
+ /// Get the etag of data of a post.
+ /// </summary>
+ /// <param name="name">See remarks of <see cref="IBaseTimelineService"/>.</param>
+ /// <param name="postId">The id of the post.</param>
+ /// <returns>The etag of the data.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="name"/> is null.</exception>
+ /// <exception cref="ArgumentException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelineNotExistException">See remarks of <see cref="IBaseTimelineService"/>.</exception>
+ /// <exception cref="TimelinePostNotExistException">Thrown when post of <paramref name="postId"/> does not exist or has been deleted.</exception>
+ /// <exception cref="InvalidOperationException">Thrown when post has no data. See remarks.</exception>
+ /// <seealso cref="GetPostData(string, long)"/>
+ Task<string> GetPostDataETag(string name, long postId);
+
+ /// <summary>
/// Get the data of a post.
/// </summary>
/// <param name="name">See remarks of <see cref="IBaseTimelineService"/>.</param>
@@ -105,6 +120,7 @@ namespace Timeline.Services /// <remarks>
/// Use this method to retrieve the image of image post.
/// </remarks>
+ /// <seealso cref="GetPostDataETag(string, long)"/>
Task<PostData> GetPostData(string name, long postId);
/// <summary>
@@ -402,6 +418,29 @@ namespace Timeline.Services }
return posts;
}
+
+ public async Task<string> 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<PostData> GetPostData(string name, long postId)
{
if (name == null)
@@ -1014,6 +1053,12 @@ namespace Timeline.Services return s.GetPosts(realName);
}
+ public Task<string> GetPostDataETag(string name, long postId)
+ {
+ var s = BranchName(name, out var realName);
+ return s.GetPostDataETag(realName, postId);
+ }
+
public Task<PostData> GetPostData(string name, long postId)
{
var s = BranchName(name, out var realName);
diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs index 1b1be698..3ab8f14d 100644 --- a/Timeline/Services/UserAvatarService.cs +++ b/Timeline/Services/UserAvatarService.cs @@ -22,6 +22,11 @@ namespace Timeline.Services {
public Avatar Avatar { get; set; } = default!;
public DateTime LastModified { get; set; }
+
+ public CacheableData ToCacheableData()
+ {
+ return new CacheableData(Avatar.Type, Avatar.Data, LastModified);
+ }
}
/// <summary>
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<IActionContextAccessor, ActionContextAccessor>();
- services.AddDbContext<DatabaseContext>((services, options )=>
+ services.AddDbContext<DatabaseContext>((services, options) =>
{
var pathProvider = services.GetRequiredService<IPathProvider>();
options.UseSqlite($"Data Source={pathProvider.GetDatabaseFilePath()}");
diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index 245ff3e7..739f79dd 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -72,6 +72,11 @@ <AutoGen>True</AutoGen>
<DependentUpon>Filters.resx</DependentUpon>
</Compile>
+ <Compile Update="Resources\Helper\DataCacheHelper.Designer.cs">
+ <DesignTime>True</DesignTime>
+ <AutoGen>True</AutoGen>
+ <DependentUpon>DataCacheHelper.resx</DependentUpon>
+ </Compile>
<Compile Update="Resources\Messages.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
@@ -159,6 +164,10 @@ <Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Filters.Designer.cs</LastGenOutput>
</EmbeddedResource>
+ <EmbeddedResource Update="Resources\Helper\DataCacheHelper.resx">
+ <Generator>ResXFileCodeGenerator</Generator>
+ <LastGenOutput>DataCacheHelper.Designer.cs</LastGenOutput>
+ </EmbeddedResource>
<EmbeddedResource Update="Resources\Messages.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Messages.Designer.cs</LastGenOutput>
|