From 62a3557ab62e1fa188e7498643d7cf0221a18322 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Wed, 21 Aug 2019 18:33:07 +0800 Subject: Add database entity and service. --- Timeline/Entities/DatabaseContext.cs | 4 +- Timeline/Entities/UserDetail.cs | 26 +++++++ Timeline/Models/UserDetail.cs | 31 ++++++++ Timeline/Models/Validation/UserDetailValidator.cs | 11 +++ Timeline/Services/DatabaseExtensions.cs | 30 ++++++++ Timeline/Services/UserAvatarService.cs | 21 +----- Timeline/Services/UserDetailService.cs | 90 +++++++++++++++++++++++ 7 files changed, 194 insertions(+), 19 deletions(-) create mode 100644 Timeline/Entities/UserDetail.cs create mode 100644 Timeline/Models/UserDetail.cs create mode 100644 Timeline/Models/Validation/UserDetailValidator.cs create mode 100644 Timeline/Services/DatabaseExtensions.cs create mode 100644 Timeline/Services/UserDetailService.cs (limited to 'Timeline') diff --git a/Timeline/Entities/DatabaseContext.cs b/Timeline/Entities/DatabaseContext.cs index 6e1fc638..d9815660 100644 --- a/Timeline/Entities/DatabaseContext.cs +++ b/Timeline/Entities/DatabaseContext.cs @@ -28,8 +28,9 @@ namespace Timeline.Entities [Column("version"), Required] public long Version { get; set; } - [Required] public UserAvatar Avatar { get; set; } + + public UserDetailEntity Detail { get; set; } } public class DatabaseContext : DbContext @@ -48,5 +49,6 @@ namespace Timeline.Entities public DbSet Users { get; set; } public DbSet UserAvatars { get; set; } + public DbSet UserDetails { get; set; } } } diff --git a/Timeline/Entities/UserDetail.cs b/Timeline/Entities/UserDetail.cs new file mode 100644 index 00000000..ee829717 --- /dev/null +++ b/Timeline/Entities/UserDetail.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Timeline.Entities +{ + [Table("user_details")] + public class UserDetailEntity + { + [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + [Column("qq"), MaxLength(15)] + public string QQ { get; set; } + + [Column("email"), MaxLength(30)] + public string EMail { get; set; } + + [Column("phone_number"), MaxLength(15)] + public string PhoneNumber { get; set; } + + [Column("description")] + public string Description { get; set; } + + public long UserId { get; set; } + } +} diff --git a/Timeline/Models/UserDetail.cs b/Timeline/Models/UserDetail.cs new file mode 100644 index 00000000..91439c6a --- /dev/null +++ b/Timeline/Models/UserDetail.cs @@ -0,0 +1,31 @@ +using Timeline.Entities; + +namespace Timeline.Models +{ + public class UserDetail + { + public string QQ { get; set; } + public string EMail { get; set; } + public string PhoneNumber { get; set; } + public string Description { get; set; } + + private static string CoerceEmptyToNull(string value) + { + if (string.IsNullOrEmpty(value)) + return null; + else + return value; + } + + public static UserDetail From(UserDetailEntity entity) + { + return new UserDetail + { + QQ = CoerceEmptyToNull(entity.QQ), + EMail = CoerceEmptyToNull(entity.EMail), + PhoneNumber = CoerceEmptyToNull(entity.PhoneNumber), + Description = CoerceEmptyToNull(entity.Description) + }; + } + } +} diff --git a/Timeline/Models/Validation/UserDetailValidator.cs b/Timeline/Models/Validation/UserDetailValidator.cs new file mode 100644 index 00000000..5fdaec00 --- /dev/null +++ b/Timeline/Models/Validation/UserDetailValidator.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Timeline.Models.Validation +{ + public class UserDetailValidator + { + } +} diff --git a/Timeline/Services/DatabaseExtensions.cs b/Timeline/Services/DatabaseExtensions.cs new file mode 100644 index 00000000..a37cf05b --- /dev/null +++ b/Timeline/Services/DatabaseExtensions.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; + +namespace Timeline.Services +{ + public static class DatabaseExtensions + { + /// + /// Check the existence and get the id of the user. + /// + /// The username of the user. + /// The user id. + /// Thrown if is null or empty. + /// Thrown if user does not exist. + public static async Task CheckAndGetUser(DbSet userDbSet, string username) + { + if (string.IsNullOrEmpty(username)) + throw new ArgumentException("Username is null or empty.", nameof(username)); + + var userId = await userDbSet.Where(u => u.Name == username).Select(u => u.Id).SingleOrDefaultAsync(); + if (userId == 0) + throw new UserNotExistException(username); + return userId; + } + } +} diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs index 7b1f405c..5c380dd8 100644 --- a/Timeline/Services/UserAvatarService.cs +++ b/Timeline/Services/UserAvatarService.cs @@ -219,12 +219,7 @@ namespace Timeline.Services public async Task GetAvatarETag(string username) { - if (string.IsNullOrEmpty(username)) - throw new ArgumentException("Username is null or empty.", nameof(username)); - - var userId = await _database.Users.Where(u => u.Name == username).Select(u => u.Id).SingleOrDefaultAsync(); - if (userId == 0) - throw new UserNotExistException(username); + var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); var eTag = (await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.ETag }).SingleAsync()).ETag; if (eTag == null) @@ -235,12 +230,7 @@ namespace Timeline.Services public async Task GetAvatar(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 userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); var avatar = await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.Type, a.Data, a.LastModified }).SingleAsync(); @@ -272,9 +262,6 @@ namespace Timeline.Services public async Task SetAvatar(string username, Avatar avatar) { - if (string.IsNullOrEmpty(username)) - throw new ArgumentException("Username is null or empty.", nameof(username)); - if (avatar != null) { if (string.IsNullOrEmpty(avatar.Type)) @@ -283,9 +270,7 @@ namespace Timeline.Services throw new ArgumentException("Data of avatar is null.", nameof(avatar)); } - var userId = await _database.Users.Where(u => u.Name == username).Select(u => u.Id).SingleOrDefaultAsync(); - if (userId == 0) - throw new UserNotExistException(username); + var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleAsync(); diff --git a/Timeline/Services/UserDetailService.cs b/Timeline/Services/UserDetailService.cs new file mode 100644 index 00000000..c3a2a1af --- /dev/null +++ b/Timeline/Services/UserDetailService.cs @@ -0,0 +1,90 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Models; + +namespace Timeline.Services +{ + public interface IUserDetailService + { + /// + /// Get the detail of user. + /// + /// The username to get user detail of. + /// The user detail. + /// Thrown if is null or empty. + /// Thrown if user doesn't exist. + Task GetUserDetail(string username); + + /// + /// Update the detail of user. This function does not do data check. + /// + /// The username to get user detail of. + /// The detail to update. Can't be null. Any null member means not set. + /// Thrown if is null or empty or is null. + /// Thrown if user doesn't exist. + Task UpdateUserDetail(string username, UserDetail detail); + } + + public class UserDetailService : IUserDetailService + { + private readonly ILogger _logger; + + private readonly DatabaseContext _databaseContext; + + public UserDetailService(ILogger logger, DatabaseContext databaseContext) + { + _logger = logger; + _databaseContext = databaseContext; + } + + // Check the existence of user detail entry + private async Task CheckAndInit(long userId) + { + var detail = await _databaseContext.UserDetails.Where(e => e.UserId == userId).SingleOrDefaultAsync(); + if (detail == null) + { + detail = new UserDetailEntity() + { + UserId = userId + }; + _databaseContext.UserDetails.Add(detail); + await _databaseContext.SaveChangesAsync(); + } + return detail; + } + + public async Task GetUserDetail(string username) + { + var userId = await DatabaseExtensions.CheckAndGetUser(_databaseContext.Users, username); + var detailEntity = await CheckAndInit(userId); + return UserDetail.From(detailEntity); + } + + public async Task UpdateUserDetail(string username, UserDetail detail) + { + if (detail == null) + throw new ArgumentNullException(nameof(detail)); + + var userId = await DatabaseExtensions.CheckAndGetUser(_databaseContext.Users, username); + var detailEntity = await CheckAndInit(userId); + + if (detail.QQ != null) + detailEntity.QQ = detail.QQ; + + if (detail.EMail != null) + detailEntity.EMail = detail.EMail; + + if (detail.PhoneNumber != null) + detailEntity.PhoneNumber = detail.PhoneNumber; + + if (detail.Description != null) + detailEntity.Description = detail.Description; + + await _databaseContext.SaveChangesAsync(); + } + } +} -- cgit v1.2.3 From dbc8d78dc0f21f423a0fe2db9dd782da43b5b468 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Wed, 21 Aug 2019 22:36:04 +0800 Subject: Add database test for user detail. --- Timeline.Tests/DatabaseTest.cs | 17 ++++++++++++++++- Timeline/Services/UserDetailService.cs | 2 ++ 2 files changed, 18 insertions(+), 1 deletion(-) (limited to 'Timeline') diff --git a/Timeline.Tests/DatabaseTest.cs b/Timeline.Tests/DatabaseTest.cs index e280637c..f75ab71b 100644 --- a/Timeline.Tests/DatabaseTest.cs +++ b/Timeline.Tests/DatabaseTest.cs @@ -1,5 +1,4 @@ using FluentAssertions; -using Microsoft.EntityFrameworkCore; using System; using System.Linq; using Timeline.Entities; @@ -33,5 +32,21 @@ namespace Timeline.Tests _context.SaveChanges(); _context.UserAvatars.Count().Should().Be(1); } + + [Fact] + public void DeleteUserShouldAlsoDeleteDetail() + { + var user = _context.Users.First(); + _context.UserDetails.Add(new UserDetailEntity + { + UserId = user.Id + }); + _context.SaveChanges(); + _context.UserDetails.Count().Should().Be(1); + + _context.Users.Remove(user); + _context.SaveChanges(); + _context.UserDetails.Count().Should().Be(0); + } } } diff --git a/Timeline/Services/UserDetailService.cs b/Timeline/Services/UserDetailService.cs index c3a2a1af..d1fdc040 100644 --- a/Timeline/Services/UserDetailService.cs +++ b/Timeline/Services/UserDetailService.cs @@ -53,6 +53,7 @@ namespace Timeline.Services }; _databaseContext.UserDetails.Add(detail); await _databaseContext.SaveChangesAsync(); + _logger.LogInformation("An entity is created in user_details."); } return detail; } @@ -85,6 +86,7 @@ namespace Timeline.Services detailEntity.Description = detail.Description; await _databaseContext.SaveChangesAsync(); + _logger.LogInformation("An entity is updated in user_details."); } } } -- cgit v1.2.3 From e7714754d8f59bb8dc29aeb6340380e93310175b Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Wed, 21 Aug 2019 23:55:42 +0800 Subject: Add validators. --- Timeline.Tests/UserDetailValidatorTest.cs | 97 +++++++++++++++++++ Timeline.Tests/UsernameValidatorUnitTest.cs | 1 - Timeline/Entities/UserDetail.cs | 2 +- Timeline/Models/UserDetail.cs | 7 ++ Timeline/Models/Validation/UserDetailValidator.cs | 113 +++++++++++++++++++++- 5 files changed, 214 insertions(+), 6 deletions(-) create mode 100644 Timeline.Tests/UserDetailValidatorTest.cs (limited to 'Timeline') diff --git a/Timeline.Tests/UserDetailValidatorTest.cs b/Timeline.Tests/UserDetailValidatorTest.cs new file mode 100644 index 00000000..9b112946 --- /dev/null +++ b/Timeline.Tests/UserDetailValidatorTest.cs @@ -0,0 +1,97 @@ +using FluentAssertions; +using System.Collections.Generic; +using Timeline.Models.Validation; +using Xunit; + +namespace Timeline.Tests +{ + public static class UserDetailValidatorsTest + { + private static void SucceedWith(object value) where TValidator : class, IValidator, new() + { + var result = new TValidator().Validate(value, out var message); + result.Should().BeTrue(); + message.Should().Equals(ValidationConstants.SuccessMessage); + } + + private static void FailWith(object value, params string[] messageContains) where TValidator : class, IValidator, new() + { + var result = new TValidator().Validate(value, out var message); + result.Should().BeFalse(); + + foreach (var m in messageContains) + { + message.Should().ContainEquivalentOf(m); + } + } + + public class QQ + { + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("12345678")] + public void Success(object qq) + { + SucceedWith(qq); + } + + [Theory] + [InlineData(123, "type")] + [InlineData("123", "short")] + [InlineData("111111111111111111111111111111111111", "long")] + [InlineData("aaaaaaaa", "digit")] + public void Fail(object qq, string messageContains) + { + FailWith(qq, messageContains); + } + } + + public class EMail + { + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("aaa@aaa.net")] + public void Success(object email) + { + SucceedWith(email); + } + + public static IEnumerable FailTestData() + { + yield return new object[] { 123, "type" }; + yield return new object[] { new string('a', 100), "long" }; + yield return new object[] { "aaaaaaaa", "format" }; + } + + [Theory] + [MemberData(nameof(FailTestData))] + public void Fail(object email, string messageContains) + { + FailWith(email, messageContains); + } + } + + public class PhoneNumber + { + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("12345678910")] + public void Success(object phoneNumber) + { + SucceedWith(phoneNumber); + } + + [Theory] + [InlineData(123, "type")] + [InlineData("111111111111111111111111111111111111", "long")] + [InlineData("aaaaaaaa", "digit")] + public void Fail(object phoneNumber, string messageContains) + { + FailWith(phoneNumber, messageContains); + } + } + } +} diff --git a/Timeline.Tests/UsernameValidatorUnitTest.cs b/Timeline.Tests/UsernameValidatorUnitTest.cs index 20558d0e..6a635ba1 100644 --- a/Timeline.Tests/UsernameValidatorUnitTest.cs +++ b/Timeline.Tests/UsernameValidatorUnitTest.cs @@ -1,5 +1,4 @@ using FluentAssertions; -using System; using Timeline.Models.Validation; using Xunit; diff --git a/Timeline/Entities/UserDetail.cs b/Timeline/Entities/UserDetail.cs index ee829717..9bc6f5e5 100644 --- a/Timeline/Entities/UserDetail.cs +++ b/Timeline/Entities/UserDetail.cs @@ -12,7 +12,7 @@ namespace Timeline.Entities [Column("qq"), MaxLength(15)] public string QQ { get; set; } - [Column("email"), MaxLength(30)] + [Column("email"), MaxLength(50)] public string EMail { get; set; } [Column("phone_number"), MaxLength(15)] diff --git a/Timeline/Models/UserDetail.cs b/Timeline/Models/UserDetail.cs index 91439c6a..4af88450 100644 --- a/Timeline/Models/UserDetail.cs +++ b/Timeline/Models/UserDetail.cs @@ -1,12 +1,19 @@ using Timeline.Entities; +using Timeline.Models.Validation; namespace Timeline.Models { public class UserDetail { + [ValidateWith(typeof(UserDetailValidators.QQValidator))] public string QQ { get; set; } + + [ValidateWith(typeof(UserDetailValidators.EMailValidator))] public string EMail { get; set; } + + [ValidateWith(typeof(UserDetailValidators.PhoneNumberValidator))] public string PhoneNumber { get; set; } + public string Description { get; set; } private static string CoerceEmptyToNull(string value) diff --git a/Timeline/Models/Validation/UserDetailValidator.cs b/Timeline/Models/Validation/UserDetailValidator.cs index 5fdaec00..19c82edb 100644 --- a/Timeline/Models/Validation/UserDetailValidator.cs +++ b/Timeline/Models/Validation/UserDetailValidator.cs @@ -1,11 +1,116 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using System.Net.Mail; namespace Timeline.Models.Validation { - public class UserDetailValidator + public abstract class OptionalStringValidator : IValidator { + public bool Validate(object value, out string message) + { + if (value == null) + { + message = ValidationConstants.SuccessMessage; + return true; + } + + if (value is string s) + { + if (s.Length == 0) + { + message = ValidationConstants.SuccessMessage; + return true; + } + return DoValidate(s, out message); + } + else + { + message = "Value is not of type string."; + return false; + } + } + + protected abstract bool DoValidate(string value, out string message); + } + + public static class UserDetailValidators + { + + public class QQValidator : OptionalStringValidator + { + protected override bool DoValidate(string value, out string message) + { + if (value.Length < 5) + { + message = "QQ is too short."; + return false; + } + + if (value.Length > 11) + { + message = "QQ is too long."; + return false; + } + + foreach (var c in value) + { + if (!char.IsDigit(c)) + { + message = "QQ must only contain digit."; + return false; + } + } + + message = ValidationConstants.SuccessMessage; + return true; + } + } + + public class EMailValidator : OptionalStringValidator + { + protected override bool DoValidate(string value, out string message) + { + if (value.Length > 50) + { + message = "E-Mail is too long."; + return false; + } + + try + { + var _ = new MailAddress(value); + } + catch (FormatException) + { + message = "The format of E-Mail is bad."; + return false; + } + message = ValidationConstants.SuccessMessage; + return true; + } + } + + public class PhoneNumberValidator : OptionalStringValidator + { + protected override bool DoValidate(string value, out string message) + { + if (value.Length > 14) + { + message = "Phone number is too long."; + return false; + } + + foreach (var c in value) + { + if (!char.IsDigit(c)) + { + message = "Phone number can only contain digit."; + return false; + } + } + + message = ValidationConstants.SuccessMessage; + return true; + } + } } } -- cgit v1.2.3 From 1934e2d6ade6115bdb8f7f90f590b557ec96323d Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Thu, 22 Aug 2019 14:32:37 +0800 Subject: Add user detail controller. --- Timeline.Tests/IntegratedTests/UserDetailTest.cs | 123 +++++++++++++++++++++++ Timeline/Controllers/UserDetailController.cs | 75 ++++++++++++++ Timeline/Services/UserDetailService.cs | 9 ++ Timeline/Startup.cs | 1 + 4 files changed, 208 insertions(+) create mode 100644 Timeline.Tests/IntegratedTests/UserDetailTest.cs create mode 100644 Timeline/Controllers/UserDetailController.cs (limited to 'Timeline') diff --git a/Timeline.Tests/IntegratedTests/UserDetailTest.cs b/Timeline.Tests/IntegratedTests/UserDetailTest.cs new file mode 100644 index 00000000..571f200f --- /dev/null +++ b/Timeline.Tests/IntegratedTests/UserDetailTest.cs @@ -0,0 +1,123 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Testing; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Timeline.Controllers; +using Timeline.Models; +using Timeline.Models.Http; +using Timeline.Tests.Helpers; +using Timeline.Tests.Helpers.Authentication; +using Timeline.Tests.Mock.Data; +using Xunit; +using Xunit.Abstractions; + +namespace Timeline.Tests.IntegratedTests +{ + public class UserDetailTest : IClassFixture>, IDisposable + { + private readonly WebApplicationFactory _factory; + private readonly Action _disposeAction; + + public UserDetailTest(MyWebApplicationFactory factory, ITestOutputHelper outputHelper) + { + _factory = factory.WithTestConfig(outputHelper, out _disposeAction); + } + + public void Dispose() + { + _disposeAction(); + } + + [Fact] + public async Task TestAsUser() + { + using (var client = await _factory.CreateClientAsUser()) + { + { + var res = await client.GetAsync($"users/usernotexist/details"); + res.Should().HaveStatusCodeNotFound() + .And.Should().HaveBodyAsCommonResponseWithCode(UserDetailController.ErrorCodes.Get_UserNotExist); + } + + async Task GetAndTest(UserDetail d) + { + var res = await client.GetAsync($"users/{MockUsers.UserUsername}/details"); + res.Should().HaveStatusCodeOk() + .And.Should().HaveBodyAsJson() + .Which.Should().BeEquivalentTo(d); + } + + await GetAndTest(new UserDetail()); + + { + var res = await client.PatchAsJsonAsync($"users/{MockUsers.AdminUsername}/details", new UserDetail()); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden) + .And.Should().HaveBodyAsCommonResponseWithCode(UserDetailController.ErrorCodes.Patch_Forbid); + } + + { + var res = await client.PatchAsJsonAsync($"users/{MockUsers.UserUsername}/details", new UserDetail + { + QQ = "aaaaaaa", + EMail = "aaaaaa" + }); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.Should().HaveBodyAsCommonResponseWithCode(CommonResponse.ErrorCodes.InvalidModel); + } + + var detail = new UserDetail + { + QQ = "1234567", + EMail = "aaaa@aaa.net", + Description = "aaaaaaaaa" + }; + + { + var res = await client.PatchAsJsonAsync($"users/{MockUsers.UserUsername}/details", detail); + res.Should().HaveStatusCodeOk(); + await GetAndTest(detail); + } + + var detail2 = new UserDetail + { + QQ = "", + PhoneNumber = "12345678910", + Description = "bbbbbbbb" + }; + + { + var res = await client.PatchAsJsonAsync($"users/{MockUsers.UserUsername}/details", detail2); + res.Should().HaveStatusCodeOk(); + await GetAndTest(new UserDetail + { + QQ = null, + EMail = detail.EMail, + PhoneNumber = detail2.PhoneNumber, + Description = detail2.Description + }); + } + } + } + + [Fact] + public async Task TestAsAdmin() + { + using (var client = await _factory.CreateClientAsAdmin()) + { + { + var res = await client.PatchAsJsonAsync($"users/{MockUsers.UserUsername}/details", new UserDetail()); + res.Should().HaveStatusCodeOk(); + } + + { + var res = await client.PatchAsJsonAsync($"users/usernotexist/details", new UserDetail()); + res.Should().HaveStatusCodeNotFound() + .And.Should().HaveBodyAsCommonResponseWithCode(UserDetailController.ErrorCodes.Patch_UserNotExist); + } + } + } + } +} \ No newline at end of file diff --git a/Timeline/Controllers/UserDetailController.cs b/Timeline/Controllers/UserDetailController.cs new file mode 100644 index 00000000..9e1d5483 --- /dev/null +++ b/Timeline/Controllers/UserDetailController.cs @@ -0,0 +1,75 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; +using Timeline.Authenticate; +using Timeline.Models; +using Timeline.Models.Http; +using Timeline.Services; + +namespace Timeline.Controllers +{ + [Route("users/{username}/details")] + [ProducesErrorResponseType(typeof(CommonResponse))] + [ApiController] + public class UserDetailController : Controller + { + public static class ErrorCodes + { + public const int Get_UserNotExist = -1001; + + public const int Patch_Forbid = -2001; + public const int Patch_UserNotExist = -2002; + + } + + private readonly ILogger _logger; + private readonly IUserDetailService _service; + + public UserDetailController(ILogger logger, IUserDetailService service) + { + _logger = logger; + _service = service; + } + + [HttpGet()] + [UserAuthorize] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserDetail))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Get([FromRoute] string username) + { + try + { + var detail = await _service.GetUserDetail(username); + return Ok(detail); + } + catch (UserNotExistException) + { + return NotFound(new CommonResponse(ErrorCodes.Get_UserNotExist, "The user does not exist.")); + } + } + + [HttpPatch()] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(void))] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Patch([FromRoute] string username, [FromBody] UserDetail detail) + { + if (!User.IsAdmin() && User.Identity.Name != username) + return StatusCode(StatusCodes.Status403Forbidden, new CommonResponse(ErrorCodes.Patch_Forbid, "You can't change other's details unless you are admin.")); + + try + { + await _service.UpdateUserDetail(username, detail); + return Ok(); + } + catch (UserNotExistException) + { + return NotFound(new CommonResponse(ErrorCodes.Patch_UserNotExist, "The user does not exist.")); + } + } + } +} diff --git a/Timeline/Services/UserDetailService.cs b/Timeline/Services/UserDetailService.cs index d1fdc040..0bb745f3 100644 --- a/Timeline/Services/UserDetailService.cs +++ b/Timeline/Services/UserDetailService.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; using System.Linq; @@ -89,4 +90,12 @@ namespace Timeline.Services _logger.LogInformation("An entity is updated in user_details."); } } + + public static class UserDetailServiceCollectionExtensions + { + public static void AddUserDetailService(this IServiceCollection services) + { + services.AddScoped(); + } + } } diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 66f648c3..b5a5106b 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -46,6 +46,7 @@ namespace Timeline services.AddTransient(); services.AddUserAvatarService(); + services.AddUserDetailService(); var databaseConfig = Configuration.GetSection(nameof(DatabaseConfig)).Get(); -- cgit v1.2.3 From 9112f288f638f34aab48fdb965ff9703a99adb4d Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Thu, 22 Aug 2019 14:54:47 +0800 Subject: Add nickname. Step 1. --- Timeline.Tests/UserDetailServiceTest.cs | 70 +++++++++++++++++++++++++++++++-- Timeline/Entities/UserDetail.cs | 3 ++ Timeline/Models/UserDetail.cs | 7 +++- Timeline/Services/UserDetailService.cs | 48 ++++++++++++++++++---- 4 files changed, 117 insertions(+), 11 deletions(-) (limited to 'Timeline') diff --git a/Timeline.Tests/UserDetailServiceTest.cs b/Timeline.Tests/UserDetailServiceTest.cs index 292b528b..f9170c42 100644 --- a/Timeline.Tests/UserDetailServiceTest.cs +++ b/Timeline.Tests/UserDetailServiceTest.cs @@ -34,6 +34,68 @@ namespace Timeline.Tests _database.Dispose(); } + [Fact] + public void GetNickname_ShouldThrow_ArgumentException() + { + // no need to await because arguments are checked syncronizedly. + _service.Invoking(s => s.GetUserNickname(null)).Should().Throw() + .Where(e => e.ParamName == "username" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase)); + _service.Invoking(s => s.GetUserNickname("")).Should().Throw() + .Where(e => e.ParamName == "username" && e.Message.Contains("empty", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public void GetNickname_ShouldThrow_UserNotExistException() + { + const string username = "usernotexist"; + _service.Awaiting(s => s.GetUserNickname(username)).Should().Throw() + .Where(e => e.Username == username); + } + + [Fact] + public async Task GetNickname_Should_Create_And_ReturnDefault() + { + { + var nickname = await _service.GetUserNickname(MockUsers.UserUsername); + nickname.Should().BeNull(); + } + + { + var context = _database.DatabaseContext; + var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUsers.UserUsername); + var detail = context.UserDetails.Where(e => e.UserId == userId).Single(); + detail.Nickname.Should().BeNullOrEmpty(); + detail.QQ.Should().BeNullOrEmpty(); + detail.EMail.Should().BeNullOrEmpty(); + detail.PhoneNumber.Should().BeNullOrEmpty(); + detail.Description.Should().BeNullOrEmpty(); + } + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("nickname")] + public async Task GetNickname_Should_ReturnData(string nickname) + { + { + var context = _database.DatabaseContext; + var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUsers.UserUsername); + var entity = new UserDetailEntity + { + Nickname = nickname, + UserId = userId + }; + context.Add(entity); + await context.SaveChangesAsync(); + } + + { + var n = await _service.GetUserNickname(MockUsers.UserUsername); + n.Should().Equals(string.IsNullOrEmpty(nickname) ? null : nickname); + } + } + [Fact] public void GetDetail_ShouldThrow_ArgumentException() { @@ -64,6 +126,7 @@ namespace Timeline.Tests var context = _database.DatabaseContext; var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUsers.UserUsername); var detail = context.UserDetails.Where(e => e.UserId == userId).Single(); + detail.Nickname.Should().BeNullOrEmpty(); detail.QQ.Should().BeNullOrEmpty(); detail.EMail.Should().BeNullOrEmpty(); detail.PhoneNumber.Should().BeNullOrEmpty(); @@ -77,12 +140,11 @@ namespace Timeline.Tests const string email = "ha@aaa.net"; const string description = "hahaha"; - var context = _database.DatabaseContext; - UserDetailEntity entity; { + var context = _database.DatabaseContext; var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUsers.UserUsername); - entity = new UserDetailEntity + var entity = new UserDetailEntity { EMail = email, Description = description, @@ -130,6 +192,7 @@ namespace Timeline.Tests var context = _database.DatabaseContext; var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUsers.UserUsername); var entity = context.UserDetails.Where(e => e.UserId == userId).Single(); + entity.Nickname.Should().BeNullOrEmpty(); entity.QQ.Should().BeNullOrEmpty(); entity.EMail.Should().BeNullOrEmpty(); entity.PhoneNumber.Should().BeNullOrEmpty(); @@ -137,6 +200,7 @@ namespace Timeline.Tests } [Theory] + [InlineData(nameof(UserDetail.Nickname), nameof(UserDetailEntity.Nickname), "aaaa", "bbbb")] [InlineData(nameof(UserDetail.QQ), nameof(UserDetailEntity.QQ), "12345678910", "987654321")] [InlineData(nameof(UserDetail.EMail), nameof(UserDetailEntity.EMail), "aaa@aaa.aaa", "bbb@bbb.bbb")] [InlineData(nameof(UserDetail.PhoneNumber), nameof(UserDetailEntity.PhoneNumber), "12345678910", "987654321")] diff --git a/Timeline/Entities/UserDetail.cs b/Timeline/Entities/UserDetail.cs index 9bc6f5e5..6e582234 100644 --- a/Timeline/Entities/UserDetail.cs +++ b/Timeline/Entities/UserDetail.cs @@ -9,6 +9,9 @@ namespace Timeline.Entities [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public long Id { get; set; } + [Column("nickname"), MaxLength(15)] + public string Nickname { get; set; } + [Column("qq"), MaxLength(15)] public string QQ { get; set; } diff --git a/Timeline/Models/UserDetail.cs b/Timeline/Models/UserDetail.cs index 4af88450..86866d8b 100644 --- a/Timeline/Models/UserDetail.cs +++ b/Timeline/Models/UserDetail.cs @@ -1,10 +1,14 @@ -using Timeline.Entities; +using System.ComponentModel.DataAnnotations; +using Timeline.Entities; using Timeline.Models.Validation; namespace Timeline.Models { public class UserDetail { + [MaxLength(10)] + public string Nickname { get; set; } + [ValidateWith(typeof(UserDetailValidators.QQValidator))] public string QQ { get; set; } @@ -28,6 +32,7 @@ namespace Timeline.Models { return new UserDetail { + Nickname = CoerceEmptyToNull(entity.Nickname), QQ = CoerceEmptyToNull(entity.QQ), EMail = CoerceEmptyToNull(entity.EMail), PhoneNumber = CoerceEmptyToNull(entity.PhoneNumber), diff --git a/Timeline/Services/UserDetailService.cs b/Timeline/Services/UserDetailService.cs index 0bb745f3..a8ed662b 100644 --- a/Timeline/Services/UserDetailService.cs +++ b/Timeline/Services/UserDetailService.cs @@ -11,6 +11,15 @@ namespace Timeline.Services { public interface IUserDetailService { + /// + /// Get the nickname of user. + /// + /// The username to get nickname of. + /// The user's nickname. Null if not set. + /// Thrown if is null or empty. + /// Thrown if user doesn't exist. + Task GetUserNickname(string username); + /// /// Get the detail of user. /// @@ -42,23 +51,45 @@ namespace Timeline.Services _databaseContext = databaseContext; } + private async Task CreateEntity(long userId) + { + var entity = new UserDetailEntity() + { + UserId = userId + }; + _databaseContext.UserDetails.Add(entity); + await _databaseContext.SaveChangesAsync(); + _logger.LogInformation("An entity is created in user_details."); + return entity; + } + // Check the existence of user detail entry private async Task CheckAndInit(long userId) { var detail = await _databaseContext.UserDetails.Where(e => e.UserId == userId).SingleOrDefaultAsync(); if (detail == null) { - detail = new UserDetailEntity() - { - UserId = userId - }; - _databaseContext.UserDetails.Add(detail); - await _databaseContext.SaveChangesAsync(); - _logger.LogInformation("An entity is created in user_details."); + detail = await CreateEntity(userId); } return detail; } + public async Task GetUserNickname(string username) + { + var userId = await DatabaseExtensions.CheckAndGetUser(_databaseContext.Users, username); + var detail = await _databaseContext.UserDetails.Where(e => e.UserId == userId).Select(e => new { e.Nickname }).SingleOrDefaultAsync(); + if (detail == null) + { + var entity = await CreateEntity(userId); + return null; + } + else + { + var nickname = detail.Nickname; + return string.IsNullOrEmpty(nickname) ? null : nickname; + } + } + public async Task GetUserDetail(string username) { var userId = await DatabaseExtensions.CheckAndGetUser(_databaseContext.Users, username); @@ -74,6 +105,9 @@ namespace Timeline.Services var userId = await DatabaseExtensions.CheckAndGetUser(_databaseContext.Users, username); var detailEntity = await CheckAndInit(userId); + if (detail.Nickname != null) + detailEntity.Nickname = detail.Nickname; + if (detail.QQ != null) detailEntity.QQ = detail.QQ; -- cgit v1.2.3 From 0f4b5672fd0bc10d52a6ea98fc8dae209852f7c0 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Thu, 22 Aug 2019 15:06:15 +0800 Subject: Add nickname in detail controller. --- Timeline.Tests/IntegratedTests/UserDetailTest.cs | 33 ++++++++++++++++++++---- Timeline/Controllers/UserDetailController.cs | 27 ++++++++++++++++--- 2 files changed, 52 insertions(+), 8 deletions(-) (limited to 'Timeline') diff --git a/Timeline.Tests/IntegratedTests/UserDetailTest.cs b/Timeline.Tests/IntegratedTests/UserDetailTest.cs index 571f200f..4923cd06 100644 --- a/Timeline.Tests/IntegratedTests/UserDetailTest.cs +++ b/Timeline.Tests/IntegratedTests/UserDetailTest.cs @@ -1,8 +1,6 @@ using FluentAssertions; using Microsoft.AspNetCore.Mvc.Testing; using System; -using System.Collections.Generic; -using System.Linq; using System.Net; using System.Threading.Tasks; using Timeline.Controllers; @@ -36,6 +34,12 @@ namespace Timeline.Tests.IntegratedTests { using (var client = await _factory.CreateClientAsUser()) { + { + var res = await client.GetAsync($"users/usernotexist/nickname"); + res.Should().HaveStatusCodeNotFound() + .And.Should().HaveBodyAsCommonResponseWithCode(UserDetailController.ErrorCodes.GetNickname_UserNotExist); + } + { var res = await client.GetAsync($"users/usernotexist/details"); res.Should().HaveStatusCodeNotFound() @@ -61,15 +65,24 @@ namespace Timeline.Tests.IntegratedTests { var res = await client.PatchAsJsonAsync($"users/{MockUsers.UserUsername}/details", new UserDetail { + Nickname = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", QQ = "aaaaaaa", - EMail = "aaaaaa" + EMail = "aaaaaa", + PhoneNumber = "aaaaaaaa" }); - res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveBodyAsCommonResponseWithCode(CommonResponse.ErrorCodes.InvalidModel); + var body = res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.Should().HaveBodyAsCommonResponse().Which; + body.Code.Should().Be(CommonResponse.ErrorCodes.InvalidModel); + foreach (var key in new string[] { "nickname", "qq", "email", "phonenumber" }) + { + body.Message.Should().ContainEquivalentOf(key); + } } + var detail = new UserDetail { + Nickname = "aaa", QQ = "1234567", EMail = "aaaa@aaa.net", Description = "aaaaaaaaa" @@ -81,6 +94,15 @@ namespace Timeline.Tests.IntegratedTests await GetAndTest(detail); } + { + var res = await client.GetAsync($"users/{MockUsers.UserUsername}/nickname"); + res.Should().HaveStatusCodeOk().And.Should().HaveBodyAsJson() + .Which.Should().BeEquivalentTo(new UserDetail + { + Nickname = detail.Nickname + }); + } + var detail2 = new UserDetail { QQ = "", @@ -93,6 +115,7 @@ namespace Timeline.Tests.IntegratedTests res.Should().HaveStatusCodeOk(); await GetAndTest(new UserDetail { + Nickname = detail.Nickname, QQ = null, EMail = detail.EMail, PhoneNumber = detail2.PhoneNumber, diff --git a/Timeline/Controllers/UserDetailController.cs b/Timeline/Controllers/UserDetailController.cs index 9e1d5483..5e1183c1 100644 --- a/Timeline/Controllers/UserDetailController.cs +++ b/Timeline/Controllers/UserDetailController.cs @@ -10,7 +10,7 @@ using Timeline.Services; namespace Timeline.Controllers { - [Route("users/{username}/details")] + [Route("users/{username}")] [ProducesErrorResponseType(typeof(CommonResponse))] [ApiController] public class UserDetailController : Controller @@ -22,6 +22,7 @@ namespace Timeline.Controllers public const int Patch_Forbid = -2001; public const int Patch_UserNotExist = -2002; + public const int GetNickname_UserNotExist = -3001; } private readonly ILogger _logger; @@ -33,7 +34,27 @@ namespace Timeline.Controllers _service = service; } - [HttpGet()] + [HttpGet("nickname")] + [UserAuthorize] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserDetail))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetNickname([FromRoute] string username) + { + try + { + var nickname = await _service.GetUserNickname(username); + return Ok(new UserDetail + { + Nickname = nickname + }); + } + catch (UserNotExistException) + { + return NotFound(new CommonResponse(ErrorCodes.GetNickname_UserNotExist, "The user does not exist.")); + } + } + + [HttpGet("details")] [UserAuthorize] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UserDetail))] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -50,7 +71,7 @@ namespace Timeline.Controllers } } - [HttpPatch()] + [HttpPatch("details")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(void))] [ProducesResponseType(StatusCodes.Status400BadRequest)] -- cgit v1.2.3 From 96c18fb2e17c94ff04094608c705db087400f510 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Thu, 22 Aug 2019 15:25:58 +0800 Subject: Update database. --- .../20190822072156_AddUserDetail.Designer.cs | 136 +++++++++++++++++++++ .../Migrations/20190822072156_AddUserDetail.cs | 47 +++++++ .../Migrations/DatabaseContextModelSnapshot.cs | 43 +++++++ 3 files changed, 226 insertions(+) create mode 100644 Timeline/Migrations/20190822072156_AddUserDetail.Designer.cs create mode 100644 Timeline/Migrations/20190822072156_AddUserDetail.cs (limited to 'Timeline') diff --git a/Timeline/Migrations/20190822072156_AddUserDetail.Designer.cs b/Timeline/Migrations/20190822072156_AddUserDetail.Designer.cs new file mode 100644 index 00000000..2bbcf673 --- /dev/null +++ b/Timeline/Migrations/20190822072156_AddUserDetail.Designer.cs @@ -0,0 +1,136 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Entities; + +namespace Timeline.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20190822072156_AddUserDetail")] + partial class AddUserDetail + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Timeline.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id"); + + b.Property("EncryptedPassword") + .IsRequired() + .HasColumnName("password"); + + b.Property("Name") + .IsRequired() + .HasColumnName("name") + .HasMaxLength(26); + + b.Property("RoleString") + .IsRequired() + .HasColumnName("roles"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnName("version") + .HasDefaultValue(0L); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatar", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id"); + + b.Property("Data") + .HasColumnName("data"); + + b.Property("ETag") + .HasColumnName("etag") + .HasMaxLength(30); + + b.Property("LastModified") + .HasColumnName("last_modified"); + + b.Property("Type") + .HasColumnName("type"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserDetailEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id"); + + b.Property("Description") + .HasColumnName("description"); + + b.Property("EMail") + .HasColumnName("email") + .HasMaxLength(50); + + b.Property("Nickname") + .HasColumnName("nickname") + .HasMaxLength(15); + + b.Property("PhoneNumber") + .HasColumnName("phone_number") + .HasMaxLength(15); + + b.Property("QQ") + .HasColumnName("qq") + .HasMaxLength(15); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_details"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatar", b => + { + b.HasOne("Timeline.Entities.User") + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatar", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Timeline.Entities.UserDetailEntity", b => + { + b.HasOne("Timeline.Entities.User") + .WithOne("Detail") + .HasForeignKey("Timeline.Entities.UserDetailEntity", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Timeline/Migrations/20190822072156_AddUserDetail.cs b/Timeline/Migrations/20190822072156_AddUserDetail.cs new file mode 100644 index 00000000..4aa6446b --- /dev/null +++ b/Timeline/Migrations/20190822072156_AddUserDetail.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Timeline.Migrations +{ + public partial class AddUserDetail : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "user_details", + columns: table => new + { + id = table.Column(nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + nickname = table.Column(maxLength: 15, nullable: true), + qq = table.Column(maxLength: 15, nullable: true), + email = table.Column(maxLength: 50, nullable: true), + phone_number = table.Column(maxLength: 15, nullable: true), + description = table.Column(nullable: true), + UserId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_user_details", x => x.id); + table.ForeignKey( + name: "FK_user_details_users_UserId", + column: x => x.UserId, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_user_details_UserId", + table: "user_details", + column: "UserId", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "user_details"); + } + } +} diff --git a/Timeline/Migrations/DatabaseContextModelSnapshot.cs b/Timeline/Migrations/DatabaseContextModelSnapshot.cs index 4941321c..1328b855 100644 --- a/Timeline/Migrations/DatabaseContextModelSnapshot.cs +++ b/Timeline/Migrations/DatabaseContextModelSnapshot.cs @@ -78,6 +78,41 @@ namespace Timeline.Migrations b.ToTable("user_avatars"); }); + modelBuilder.Entity("Timeline.Entities.UserDetailEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id"); + + b.Property("Description") + .HasColumnName("description"); + + b.Property("EMail") + .HasColumnName("email") + .HasMaxLength(50); + + b.Property("Nickname") + .HasColumnName("nickname") + .HasMaxLength(15); + + b.Property("PhoneNumber") + .HasColumnName("phone_number") + .HasMaxLength(15); + + b.Property("QQ") + .HasColumnName("qq") + .HasMaxLength(15); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_details"); + }); + modelBuilder.Entity("Timeline.Entities.UserAvatar", b => { b.HasOne("Timeline.Entities.User") @@ -85,6 +120,14 @@ namespace Timeline.Migrations .HasForeignKey("Timeline.Entities.UserAvatar", "UserId") .OnDelete(DeleteBehavior.Cascade); }); + + modelBuilder.Entity("Timeline.Entities.UserDetailEntity", b => + { + b.HasOne("Timeline.Entities.User") + .WithOne("Detail") + .HasForeignKey("Timeline.Entities.UserDetailEntity", "UserId") + .OnDelete(DeleteBehavior.Cascade); + }); #pragma warning restore 612, 618 } } -- cgit v1.2.3