diff options
author | crupest <crupest@outlook.com> | 2020-03-13 17:05:03 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2020-03-13 17:05:03 +0800 |
commit | 8a1ecbf49673cb2bed538ac8bc4e82691b90d973 (patch) | |
tree | 5e17bcec752435910ed20883ef93b84da7f55b80 | |
parent | d51954c2eaf07769e045270a02c19e46e01fe73f (diff) | |
download | timeline-8a1ecbf49673cb2bed538ac8bc4e82691b90d973.tar.gz timeline-8a1ecbf49673cb2bed538ac8bc4e82691b90d973.tar.bz2 timeline-8a1ecbf49673cb2bed538ac8bc4e82691b90d973.zip |
Abstract out data cache helper.
-rw-r--r-- | Timeline/Controllers/UserAvatarController.cs | 31 | ||||
-rw-r--r-- | Timeline/Helpers/DataCacheHelper.cs | 108 | ||||
-rw-r--r-- | Timeline/Resources/Controllers/UserAvatarController.Designer.cs | 27 | ||||
-rw-r--r-- | Timeline/Resources/Controllers/UserAvatarController.resx | 9 | ||||
-rw-r--r-- | Timeline/Resources/Helper/DataCacheHelper.Designer.cs | 90 | ||||
-rw-r--r-- | Timeline/Resources/Helper/DataCacheHelper.resx | 129 | ||||
-rw-r--r-- | Timeline/Services/UserAvatarService.cs | 5 | ||||
-rw-r--r-- | Timeline/Timeline.csproj | 9 |
8 files changed, 345 insertions, 63 deletions
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")]
diff --git a/Timeline/Helpers/DataCacheHelper.cs b/Timeline/Helpers/DataCacheHelper.cs new file mode 100644 index 00000000..c13aaddb --- /dev/null +++ b/Timeline/Helpers/DataCacheHelper.cs @@ -0,0 +1,108 @@ +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)
+ {
+ const string IfNonMatchHeaderKey = "If-None-Match";
+ const string ETagHeaderKey = "ETag";
+
+ 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)
+ {
+ controller.Response.Headers.Add(ETagHeaderKey, eTagValue);
+ logger.LogInformation(LogResultNotModified);
+ return controller.StatusCode(StatusCodes.Status304NotModified);
+ }
+ }
+
+ var data = await provider.GetData();
+ logger.LogInformation(LogResultData);
+ 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)
+ {
+ return GenerateActionResult(controller, new DelegateCacheableDataProvider(getDataETagDelegate, getDataDelegate));
+ }
+ }
+}
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/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/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>
|