diff options
-rw-r--r-- | Timeline.Tests/Helpers/AssertionResponseExtensions.cs | 3 | ||||
-rw-r--r-- | Timeline.Tests/IntegratedTests/UserAvatarTests.cs | 25 | ||||
-rw-r--r-- | Timeline.Tests/UserAvatarServiceTest.cs | 123 | ||||
-rw-r--r-- | Timeline/Controllers/UserAvatarController.cs | 23 | ||||
-rw-r--r-- | Timeline/Entities/UserAvatar.cs | 4 | ||||
-rw-r--r-- | Timeline/Models/Http/Common.cs | 6 | ||||
-rw-r--r-- | Timeline/Services/DatabaseCorruptedException.cs | 15 | ||||
-rw-r--r-- | Timeline/Services/ETagGenerator.cs | 33 | ||||
-rw-r--r-- | Timeline/Services/UserAvatarService.cs | 97 |
9 files changed, 281 insertions, 48 deletions
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<string> GetDefaultAvatarETag()
+ {
+ return Task.FromResult(ETag);
+ }
+
public Task<AvatarInfo> GetDefaultAvatar()
{
return Task.FromResult(AvatarInfo);
@@ -100,23 +107,55 @@ namespace Timeline.Tests public class UserAvatarServiceTest : IDisposable, IClassFixture<MockDefaultUserAvatarProvider>, IClassFixture<MockUserAvatarValidator>
{
- 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<UserAvatarService>(), _database.DatabaseContext, _mockDefaultUserAvatarProvider, mockUserAvatarValidator);
+ _eTagGenerator = new ETagGenerator();
+
+ _service = new UserAvatarService(_loggerFactory.CreateLogger<UserAvatarService>(), _database.DatabaseContext, _mockDefaultUserAvatarProvider, mockUserAvatarValidator, _eTagGenerator);
}
public void Dispose()
@@ -136,6 +177,46 @@ namespace Timeline.Tests }
[Fact]
+ public void GetAvatarETag_ShouldThrow_ArgumentException()
+ {
+ // no need to await because arguments are checked syncronizedly.
+ _service.Invoking(s => s.GetAvatarETag(null)).Should().Throw<ArgumentException>()
+ .Where(e => e.ParamName == "username" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase));
+ _service.Invoking(s => s.GetAvatarETag("")).Should().Throw<ArgumentException>()
+ .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<UserNotExistException>()
+ .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()
{
// no need to await because arguments are checked syncronizedly.
@@ -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<ArgumentException>()
+ _service.Invoking(s => s.SetAvatar(null, avatar)).Should().Throw<ArgumentException>()
.Where(e => e.ParamName == "username" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase));
- _service.Invoking(s => s.SetAvatar("", MockAvatar)).Should().Throw<ArgumentException>()
+ _service.Invoking(s => s.SetAvatar("", avatar)).Should().Throw<ArgumentException>()
.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<ArgumentException>()
@@ -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<UserNotExistException>()
+ _service.Awaiting(s => s.SetAvatar(username, ToAvatar(MockAvatarEntity1))).Should().Throw<UserNotExistException>()
.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<IActionResult> 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;
@@ -65,6 +66,12 @@ namespace Timeline.Services public interface IDefaultUserAvatarProvider
{
/// <summary>
+ /// Get the etag of default avatar.
+ /// </summary>
+ /// <returns></returns>
+ Task<string> GetDefaultAvatarETag();
+
+ /// <summary>
/// Get the default avatar.
/// </summary>
Task<AvatarInfo> GetDefaultAvatar();
@@ -83,6 +90,15 @@ namespace Timeline.Services public interface IUserAvatarService
{
/// <summary>
+ /// Get the etag of a user's avatar.
+ /// </summary>
+ /// <param name="username">The username of the user to get avatar etag of.</param>
+ /// <returns>The etag.</returns>
+ /// <exception cref="ArgumentException">Thrown if <paramref name="username"/> is null or empty.</exception>
+ /// <exception cref="UserNotExistException">Thrown if the user does not exist.</exception>
+ Task<string> GetAvatarETag(string username);
+
+ /// <summary>
/// Get avatar of a user. If the user has no avatar, a default one is returned.
/// </summary>
/// <param name="username">The username of the user to get avatar of.</param>
@@ -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<AvatarInfo> 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<string> GetDefaultAvatarETag()
+ {
+ await CheckAndInit();
+ return _cacheETag;
+ }
+
+ public async Task<AvatarInfo> 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<UserAvatarService> logger, DatabaseContext database, IDefaultUserAvatarProvider defaultUserAvatarProvider, IUserAvatarValidator avatarValidator)
+ private readonly IETagGenerator _eTagGenerator;
+
+ public UserAvatarService(
+ ILogger<UserAvatarService> logger,
+ DatabaseContext database,
+ IDefaultUserAvatarProvider defaultUserAvatarProvider,
+ IUserAvatarValidator avatarValidator,
+ IETagGenerator eTagGenerator)
{
_logger = logger;
_database = database;
_defaultUserAvatarProvider = defaultUserAvatarProvider;
_avatarValidator = avatarValidator;
+ _eTagGenerator = eTagGenerator;
+ }
+
+ public async Task<string> 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<AvatarInfo> 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<IETagGenerator, ETagGenerator>();
services.AddScoped<IUserAvatarService, UserAvatarService>();
services.AddSingleton<IDefaultUserAvatarProvider, DefaultUserAvatarProvider>();
services.AddSingleton<IUserAvatarValidator, UserAvatarValidator>();
|