diff --git a/Common/Thoughts.Domain.Base/Entities/ShortUrl.cs b/Common/Thoughts.Domain.Base/Entities/ShortUrl.cs new file mode 100644 index 0000000..82305a2 --- /dev/null +++ b/Common/Thoughts.Domain.Base/Entities/ShortUrl.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Thoughts.Domain.Base.Entities +{ + public class ShortUrl : EntityModel + { + /// Оригинальный URL + [Required] + public Uri OriginalUrl { get; set; } + + /// Псевдоним ссылки + [Required] + public string Alias { get; set; } + + /// Количество запросов ссылки + [Required] + public int Statistic { get; set; } + + /// Дата и время последего сброса статистики + [Required] + public DateTimeOffset LastReset { get; set; } + } +} diff --git a/Data/Thoughts.DAL.Entities/ShortUrl.cs b/Data/Thoughts.DAL.Entities/ShortUrl.cs index da9e13b..84fc589 100644 --- a/Data/Thoughts.DAL.Entities/ShortUrl.cs +++ b/Data/Thoughts.DAL.Entities/ShortUrl.cs @@ -17,5 +17,13 @@ public class ShortUrl:Entity /// Псевдоним ссылки [Required] public string Alias { get; set; } + + /// Количество запросов ссылки + [Required] + public int Statistic { get; set; } + + /// Дата и время последего сброса статистики + [Required] + public DateTimeOffset LastReset { get; set; } } } diff --git a/Data/Thoughts.DAL.SqlServer/Migrations/20221108160559_ShortUrlStatistic.Designer.cs b/Data/Thoughts.DAL.SqlServer/Migrations/20221108160559_ShortUrlStatistic.Designer.cs new file mode 100644 index 0000000..260830f --- /dev/null +++ b/Data/Thoughts.DAL.SqlServer/Migrations/20221108160559_ShortUrlStatistic.Designer.cs @@ -0,0 +1,364 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Thoughts.DAL; + +#nullable disable + +namespace Thoughts.DAL.SqlServer.Migrations +{ + [DbContext(typeof(ThoughtsDB))] + [Migration("20221108160559_ShortUrlStatistic")] + partial class ShortUrlStatistic + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("PostTag", b => + { + b.Property("PostsId") + .HasColumnType("int"); + + b.Property("TagsId") + .HasColumnType("int"); + + b.HasKey("PostsId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("PostTag"); + }); + + modelBuilder.Entity("RoleUser", b => + { + b.Property("RolesId") + .HasColumnType("int"); + + b.Property("UsersId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("RolesId", "UsersId"); + + b.HasIndex("UsersId"); + + b.ToTable("RoleUser"); + }); + + modelBuilder.Entity("Thoughts.DAL.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "Name" }, "NameIndex") + .IsUnique(); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Thoughts.DAL.Entities.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Date") + .HasColumnType("datetimeoffset"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ParentCommentId") + .HasColumnType("int"); + + b.Property("PostId") + .HasColumnType("int"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("ParentCommentId"); + + b.HasIndex("PostId"); + + b.HasIndex("UserId"); + + b.ToTable("Comments"); + }); + + modelBuilder.Entity("Thoughts.DAL.Entities.Post", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("Date") + .HasColumnType("datetimeoffset"); + + b.Property("PublicationDate") + .HasColumnType("datetimeoffset"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("UserId"); + + b.ToTable("Posts"); + }); + + modelBuilder.Entity("Thoughts.DAL.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "Name" }, "NameIndex") + .IsUnique() + .HasDatabaseName("NameIndex1"); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("Thoughts.DAL.Entities.ShortUrl", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("Alias") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastReset") + .HasColumnType("datetimeoffset"); + + b.Property("OriginalUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Statistic") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("ShortUrls"); + }); + + modelBuilder.Entity("Thoughts.DAL.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "Name" }, "NameIndex") + .IsUnique() + .HasDatabaseName("NameIndex2"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("Thoughts.DAL.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("Birthday") + .HasColumnType("date"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("NickName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Patronymic") + .HasColumnType("nvarchar(450)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "LastName", "FirstName", "Patronymic" }, "NameIndex") + .IsUnique() + .HasDatabaseName("NameIndex3") + .HasFilter("[Patronymic] IS NOT NULL"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("PostTag", b => + { + b.HasOne("Thoughts.DAL.Entities.Post", null) + .WithMany() + .HasForeignKey("PostsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Thoughts.DAL.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RoleUser", b => + { + b.HasOne("Thoughts.DAL.Entities.Role", null) + .WithMany() + .HasForeignKey("RolesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Thoughts.DAL.Entities.User", null) + .WithMany() + .HasForeignKey("UsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Thoughts.DAL.Entities.Comment", b => + { + b.HasOne("Thoughts.DAL.Entities.Comment", "ParentComment") + .WithMany("ChildrenComment") + .HasForeignKey("ParentCommentId"); + + b.HasOne("Thoughts.DAL.Entities.Post", "Post") + .WithMany("Comments") + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + + b.HasOne("Thoughts.DAL.Entities.User", "User") + .WithMany("Comments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + + b.Navigation("ParentComment"); + + b.Navigation("Post"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Thoughts.DAL.Entities.Post", b => + { + b.HasOne("Thoughts.DAL.Entities.Category", "Category") + .WithMany("Posts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Thoughts.DAL.Entities.User", "User") + .WithMany("Posts") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Thoughts.DAL.Entities.Category", b => + { + b.Navigation("Posts"); + }); + + modelBuilder.Entity("Thoughts.DAL.Entities.Comment", b => + { + b.Navigation("ChildrenComment"); + }); + + modelBuilder.Entity("Thoughts.DAL.Entities.Post", b => + { + b.Navigation("Comments"); + }); + + modelBuilder.Entity("Thoughts.DAL.Entities.User", b => + { + b.Navigation("Comments"); + + b.Navigation("Posts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Thoughts.DAL.SqlServer/Migrations/20221108160559_ShortUrlStatistic.cs b/Data/Thoughts.DAL.SqlServer/Migrations/20221108160559_ShortUrlStatistic.cs new file mode 100644 index 0000000..389617e --- /dev/null +++ b/Data/Thoughts.DAL.SqlServer/Migrations/20221108160559_ShortUrlStatistic.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Thoughts.DAL.SqlServer.Migrations +{ + public partial class ShortUrlStatistic : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LastReset", + table: "ShortUrls", + type: "datetimeoffset", + nullable: false, + defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0))); + + migrationBuilder.AddColumn( + name: "Statistic", + table: "ShortUrls", + type: "int", + nullable: false, + defaultValue: 0); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LastReset", + table: "ShortUrls"); + + migrationBuilder.DropColumn( + name: "Statistic", + table: "ShortUrls"); + } + } +} diff --git a/Data/Thoughts.DAL.SqlServer/Migrations/ThoughtsDBModelSnapshot.cs b/Data/Thoughts.DAL.SqlServer/Migrations/ThoughtsDBModelSnapshot.cs index ffdf0fe..0f4bbcb 100644 --- a/Data/Thoughts.DAL.SqlServer/Migrations/ThoughtsDBModelSnapshot.cs +++ b/Data/Thoughts.DAL.SqlServer/Migrations/ThoughtsDBModelSnapshot.cs @@ -188,10 +188,16 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("nvarchar(max)"); + b.Property("LastReset") + .HasColumnType("datetimeoffset"); + b.Property("OriginalUrl") .IsRequired() .HasColumnType("nvarchar(max)"); + b.Property("Statistic") + .HasColumnType("int"); + b.HasKey("Id"); b.ToTable("ShortUrls"); diff --git a/Data/Thoughts.DAL.SqlServer/Registrator.cs b/Data/Thoughts.DAL.SqlServer/Registrator.cs index 0701398..21d1355 100644 --- a/Data/Thoughts.DAL.SqlServer/Registrator.cs +++ b/Data/Thoughts.DAL.SqlServer/Registrator.cs @@ -9,7 +9,7 @@ public static IServiceCollection AddThoughtsDbSqlServer(this IServiceCollection { services.AddDbContext(opt => opt .UseSqlServer( - ConnectionString, + ConnectionString, o => o.MigrationsAssembly(typeof(Registrator).Assembly.FullName))); return services; diff --git a/Data/Thoughts.DAL.Sqlite/Migrations/20221108160716_ShortUrlStatistic.Designer.cs b/Data/Thoughts.DAL.Sqlite/Migrations/20221108160716_ShortUrlStatistic.Designer.cs new file mode 100644 index 0000000..42cda4d --- /dev/null +++ b/Data/Thoughts.DAL.Sqlite/Migrations/20221108160716_ShortUrlStatistic.Designer.cs @@ -0,0 +1,346 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Thoughts.DAL; + +#nullable disable + +namespace Thoughts.DAL.Sqlite.Migrations +{ + [DbContext(typeof(ThoughtsDB))] + [Migration("20221108160716_ShortUrlStatistic")] + partial class ShortUrlStatistic + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.6"); + + modelBuilder.Entity("PostTag", b => + { + b.Property("PostsId") + .HasColumnType("INTEGER"); + + b.Property("TagsId") + .HasColumnType("INTEGER"); + + b.HasKey("PostsId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("PostTag"); + }); + + modelBuilder.Entity("RoleUser", b => + { + b.Property("RolesId") + .HasColumnType("INTEGER"); + + b.Property("UsersId") + .HasColumnType("TEXT"); + + b.HasKey("RolesId", "UsersId"); + + b.HasIndex("UsersId"); + + b.ToTable("RoleUser"); + }); + + modelBuilder.Entity("Thoughts.DAL.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "Name" }, "NameIndex") + .IsUnique(); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Thoughts.DAL.Entities.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Body") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("ParentCommentId") + .HasColumnType("INTEGER"); + + b.Property("PostId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ParentCommentId"); + + b.HasIndex("PostId"); + + b.HasIndex("UserId"); + + b.ToTable("Comments"); + }); + + modelBuilder.Entity("Thoughts.DAL.Entities.Post", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Body") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("PublicationDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("UserId"); + + b.ToTable("Posts"); + }); + + modelBuilder.Entity("Thoughts.DAL.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "Name" }, "NameIndex") + .IsUnique() + .HasDatabaseName("NameIndex1"); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("Thoughts.DAL.Entities.ShortUrl", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Alias") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastReset") + .HasColumnType("TEXT"); + + b.Property("OriginalUrl") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Statistic") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("ShortUrls"); + }); + + modelBuilder.Entity("Thoughts.DAL.Entities.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "Name" }, "NameIndex") + .IsUnique() + .HasDatabaseName("NameIndex2"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("Thoughts.DAL.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Birthday") + .HasColumnType("date"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("NickName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Patronymic") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "LastName", "FirstName", "Patronymic" }, "NameIndex") + .IsUnique() + .HasDatabaseName("NameIndex3"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("PostTag", b => + { + b.HasOne("Thoughts.DAL.Entities.Post", null) + .WithMany() + .HasForeignKey("PostsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Thoughts.DAL.Entities.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("RoleUser", b => + { + b.HasOne("Thoughts.DAL.Entities.Role", null) + .WithMany() + .HasForeignKey("RolesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Thoughts.DAL.Entities.User", null) + .WithMany() + .HasForeignKey("UsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Thoughts.DAL.Entities.Comment", b => + { + b.HasOne("Thoughts.DAL.Entities.Comment", "ParentComment") + .WithMany("ChildrenComment") + .HasForeignKey("ParentCommentId"); + + b.HasOne("Thoughts.DAL.Entities.Post", "Post") + .WithMany("Comments") + .HasForeignKey("PostId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + + b.HasOne("Thoughts.DAL.Entities.User", "User") + .WithMany("Comments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.ClientNoAction) + .IsRequired(); + + b.Navigation("ParentComment"); + + b.Navigation("Post"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Thoughts.DAL.Entities.Post", b => + { + b.HasOne("Thoughts.DAL.Entities.Category", "Category") + .WithMany("Posts") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Thoughts.DAL.Entities.User", "User") + .WithMany("Posts") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Thoughts.DAL.Entities.Category", b => + { + b.Navigation("Posts"); + }); + + modelBuilder.Entity("Thoughts.DAL.Entities.Comment", b => + { + b.Navigation("ChildrenComment"); + }); + + modelBuilder.Entity("Thoughts.DAL.Entities.Post", b => + { + b.Navigation("Comments"); + }); + + modelBuilder.Entity("Thoughts.DAL.Entities.User", b => + { + b.Navigation("Comments"); + + b.Navigation("Posts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Thoughts.DAL.Sqlite/Migrations/20221108160716_ShortUrlStatistic.cs b/Data/Thoughts.DAL.Sqlite/Migrations/20221108160716_ShortUrlStatistic.cs new file mode 100644 index 0000000..1164e3c --- /dev/null +++ b/Data/Thoughts.DAL.Sqlite/Migrations/20221108160716_ShortUrlStatistic.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Thoughts.DAL.Sqlite.Migrations +{ + public partial class ShortUrlStatistic : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LastReset", + table: "ShortUrls", + type: "TEXT", + nullable: false, + defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0))); + + migrationBuilder.AddColumn( + name: "Statistic", + table: "ShortUrls", + type: "INTEGER", + nullable: false, + defaultValue: 0); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LastReset", + table: "ShortUrls"); + + migrationBuilder.DropColumn( + name: "Statistic", + table: "ShortUrls"); + } + } +} diff --git a/Data/Thoughts.DAL.Sqlite/Migrations/ThoughtsDBModelSnapshot.cs b/Data/Thoughts.DAL.Sqlite/Migrations/ThoughtsDBModelSnapshot.cs index 6856974..c2e93b8 100644 --- a/Data/Thoughts.DAL.Sqlite/Migrations/ThoughtsDBModelSnapshot.cs +++ b/Data/Thoughts.DAL.Sqlite/Migrations/ThoughtsDBModelSnapshot.cs @@ -173,10 +173,16 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("TEXT"); + b.Property("LastReset") + .HasColumnType("TEXT"); + b.Property("OriginalUrl") .IsRequired() .HasColumnType("TEXT"); + b.Property("Statistic") + .HasColumnType("INTEGER"); + b.HasKey("Id"); b.ToTable("ShortUrls"); diff --git a/Services/Thoughts.Interfaces.Base/IShortUrlManager.cs b/Services/Thoughts.Interfaces.Base/IShortUrlManager.cs deleted file mode 100644 index 1525542..0000000 --- a/Services/Thoughts.Interfaces.Base/IShortUrlManager.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Thoughts.Interfaces.Base -{ - public interface IShortUrlManager - { - /// - /// Получить оригинальный Url по псевдониму - /// - /// Псевдоним ссылки - /// Оригинальный Url - Task GetUrlAsync(string Alias, CancellationToken Cancel = default); - - /// - /// Получить оригинальный Url по идентификатору - /// - /// Идентификатор короткой ссылки - /// Оригинальный Url - Task GetUrlByIdAsync(int Id, CancellationToken Cancel = default); - - /// - /// Добавить короткую ссылку - /// - /// Добавляемый Url - /// Псевдоним ссылки - Task AddUrlAsync(string Url, CancellationToken Cancel = default); - - /// - /// Удалить короткую ссылку по идентификатору - /// - /// Идентификатор короткой ссылки - /// Результат удаления - Task DeleteUrlAsync(int Id, CancellationToken Cancel = default); - - /// - /// Обновить короткую ссылку - /// - /// Идентификатор короткой ссылки - /// Новый Url - /// Результат обновления - Task UpdateUrlAsync(int Id, string Url, CancellationToken Cancel = default); - } -} diff --git a/Services/Thoughts.Interfaces.Base/WebApiControllersPath.cs b/Services/Thoughts.Interfaces.Base/WebApiControllersPath.cs index b72c7d3..36b340d 100644 --- a/Services/Thoughts.Interfaces.Base/WebApiControllersPath.cs +++ b/Services/Thoughts.Interfaces.Base/WebApiControllersPath.cs @@ -8,6 +8,8 @@ namespace Thoughts.Interfaces.Base { static public class WebApiControllersPath { - public const string ShortUrl = "api/url"; + public const string ShortUrl = "api/v{version:apiVersion}/url"; + + public const string ShortUrlV1 = "api/v1/url"; } } diff --git a/Services/Thoughts.Interfaces/IShortUrlManager.cs b/Services/Thoughts.Interfaces/IShortUrlManager.cs new file mode 100644 index 0000000..6179e6b --- /dev/null +++ b/Services/Thoughts.Interfaces/IShortUrlManager.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Thoughts.Domain.Base.Entities; + +namespace Thoughts.Interfaces +{ + public interface IShortUrlManager + { + /// + /// Получить оригинальный Url по псевдониму + /// + /// Псевдоним ссылки + /// Токен отмены + /// Оригинальный Url + Task GetUrlAsync(string Alias, CancellationToken Cancel = default); + + /// + /// Получить оригинальный Url по идентификатору + /// + /// Идентификатор короткой ссылки + /// Токен отмены + /// Оригинальный Url + Task GetUrlByIdAsync(int Id, CancellationToken Cancel = default); + + /// + /// Получить псевдоним короткой ссылки по ее идентификатору + /// + /// Идентификатор короткой ссылки + /// Количество символов в возвращаемом псевдониме + /// Токен отмены + /// Псевдоним ссылки + Task GetAliasByIdAsync(int Id, int Length = 0, CancellationToken Cancel = default); + + /// + /// Добавить короткую ссылку + /// + /// Добавляемый Url + /// Токен отмены + /// Идентификатор добавленной короткой ссылки + Task AddUrlAsync(string Url, CancellationToken Cancel = default); + + /// + /// Удалить короткую ссылку по идентификатору + /// + /// Идентификатор короткой ссылки + /// Токен отмены + /// Результат удаления + Task DeleteUrlAsync(int Id, CancellationToken Cancel = default); + + /// + /// Обновить короткую ссылку + /// + /// Идентификатор короткой ссылки + /// Новый Url + /// Токен отмены + /// Результат обновления + Task UpdateUrlAsync(int Id, string Url, CancellationToken Cancel = default); + + /// + /// Сброс статистики использования коротких ссылок + /// + /// Идентификатор короткой ссылки для которой сбрасывается статистика. + /// Если идентификатор 0, то статистика сбрасывается для всех ссылок + /// Результат сброса статистики + Task ResetStatistic(int Id = 0, CancellationToken Cancel = default); + + /// + /// Получение статистики использования коротких ссылок + /// + /// Идентификатор короткой ссылки для которой запрашивается статистика. + /// Если идентификатор 0, то статистика запрашивается для всех ссылок + /// Перечисление коротких ссылок + Task> GetStatistic(int Id = 0, int Length = 0, CancellationToken Cancel = default); + } +} diff --git a/Services/Thoughts.Services/InSQL/SqlShortUrlManagerService.cs b/Services/Thoughts.Services/InSQL/SqlShortUrlManagerService.cs index b2eee9d..a0bb476 100644 --- a/Services/Thoughts.Services/InSQL/SqlShortUrlManagerService.cs +++ b/Services/Thoughts.Services/InSQL/SqlShortUrlManagerService.cs @@ -4,13 +4,16 @@ using System.Linq; using System.Security.Cryptography; using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Channels; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Thoughts.DAL; -using Thoughts.Interfaces.Base; +using Thoughts.Domain.Base.Entities; +using Thoughts.Interfaces; namespace Thoughts.Services.InSQL { @@ -25,16 +28,19 @@ public SqlShortUrlManagerService(ThoughtsDB Db, ILogger Logg _logger = Logger; } - public async Task AddUrlAsync(string UrlString, CancellationToken Cancel = default) - { + public async Task AddUrlAsync(string UrlString, CancellationToken Cancel = default) + { _logger.LogInformation($"Создание короткой ссылки для Url:{UrlString}"); + if (!Regex.IsMatch(UrlString, @"^https?://")) + throw new FormatException("Строка адреса не имеет схемы"); + var url = CreateUrl(UrlString); if (url is null) { _logger.LogInformation($"Короткая ссылка не создана. Некоректный Url:{UrlString}"); - return String.Empty; + return 0; } var shortUrl = await _db.ShortUrls. @@ -46,20 +52,22 @@ public async Task AddUrlAsync(string UrlString, CancellationToken Cancel if (shortUrl is not null) { _logger.LogInformation($"Короткая ссылка {shortUrl.Alias} уже существует для Url:{shortUrl.OriginalUrl}"); - return shortUrl.Alias; + return shortUrl.Id; } shortUrl = new() { OriginalUrl = url, - Alias = GenerateAlias(url.OriginalString) + Alias = GenerateAlias(url.OriginalString), + Statistic = 0, + LastReset = DateTime.UtcNow }; await _db.ShortUrls.AddAsync(shortUrl, Cancel).ConfigureAwait(false); await _db.SaveChangesAsync(Cancel).ConfigureAwait(false); _logger.LogInformation($"Создана короткая ссылка {shortUrl.Alias} для Url:{shortUrl.OriginalUrl}"); - return shortUrl.Alias; + return shortUrl.Id; } public async Task DeleteUrlAsync(int Id, CancellationToken Cancel = default) @@ -101,13 +109,16 @@ public async Task DeleteUrlAsync(int Id, CancellationToken Cancel = defaul { var result = await _db.ShortUrls. FirstOrDefaultAsync( - u => u.Alias == Alias, + u => u.Alias.StartsWith(Alias), Cancel ). ConfigureAwait(false); if (result is null) return null; + result.Statistic++; + await _db.SaveChangesAsync(Cancel).ConfigureAwait(false); + return result.OriginalUrl; } public async Task GetUrlByIdAsync(int Id, CancellationToken Cancel = default) @@ -124,6 +135,23 @@ public async Task DeleteUrlAsync(int Id, CancellationToken Cancel = defaul return result.OriginalUrl; } + public async Task GetAliasByIdAsync(int Id, int Length, CancellationToken Cancel = default) + { + var result = await _db.ShortUrls. + FirstOrDefaultAsync( + u => u.Id == Id, + Cancel + ). + ConfigureAwait(false); + if (result is null) + return null; + + if (Length > 0) + return result.Alias.Substring(0, result.Alias.Length < Length ? result.Alias.Length : Length); + + return result.Alias; + } + public async Task UpdateUrlAsync(int Id, string UrlString, CancellationToken Cancel = default) { _logger.LogInformation($"Обновление короткой ссылки Id:{Id}. Новый Url:{UrlString}"); @@ -161,12 +189,86 @@ public async Task UpdateUrlAsync(int Id, string UrlString, CancellationTok } catch (OperationCanceledException e) { - _logger.LogError($"Обновление короткой ссылки Id:{Id} вызвало исключение DbUpdateConcurrencyException: {e.ToString()}"); + _logger.LogError($"Обновление короткой ссылки Id:{Id} вызвало исключение OperationCanceledException: {e.ToString()}"); return false; } return true; } + public async Task ResetStatistic(int Id = 0, CancellationToken Cancel = default) + { + //Если Id==0, сбрасываем статистику для всех коротких ссылок + if (Id == 0) + { + await _db.ShortUrls.ForEachAsync(s => + { + s.Statistic = 0; + s.LastReset = DateTime.UtcNow; + }); + } + else + { + var shortUrl = await _db.ShortUrls.FirstOrDefaultAsync(s => s.Id == Id).ConfigureAwait(false); + if (shortUrl is null) + return false; + shortUrl.Statistic = 0; + shortUrl.LastReset = DateTime.UtcNow; + } + + try + { + await _db.SaveChangesAsync(Cancel).ConfigureAwait(false); + } + catch (DbUpdateException e) + { + _logger.LogError($"Обновление статистики вызвало исключение DbUpdateException: {e.ToString()}"); + return false; + } + catch (OperationCanceledException e) + { + _logger.LogError($"Обновление статистики вызвало исключение OperationCanceledException: {e.ToString()}"); + return false; + } + + return true; + } + public async Task> GetStatistic(int Id = 0, int Length=0, CancellationToken Cancel = default) + { + if (Id == 0) + { + return _db.ShortUrls.Select(s => new ShortUrl + { + Id = s.Id, + Alias = Length == 0 + ? s.Alias + : s.Alias.Substring(0, s.Alias.Length < Length + ? s.Alias.Length + : Length), + OriginalUrl = s.OriginalUrl, + LastReset = s.LastReset, + Statistic = s.Statistic + }); + } + + var shortUrl = await _db.ShortUrls.FirstOrDefaultAsync(s => s.Id == Id); + if (shortUrl is null) + return null; + + return Enumerable.Repeat(new ShortUrl + { + Id = shortUrl.Id, + Alias = Length == 0 + ? shortUrl.Alias : + shortUrl.Alias.Substring(0, shortUrl.Alias.Length < Length + ? shortUrl.Alias.Length + : Length), + OriginalUrl = shortUrl.OriginalUrl, + LastReset = shortUrl.LastReset, + Statistic = shortUrl.Statistic + }, 1); + } + + /// /// Генерирование псевдонима ссылки (хеш MD5) /// diff --git a/Services/Thoughts.WebAPI.Clients/ShortUrl/ShortUrlClient.cs b/Services/Thoughts.WebAPI.Clients/ShortUrl/ShortUrlClient.cs index 263897d..5a22b50 100644 --- a/Services/Thoughts.WebAPI.Clients/ShortUrl/ShortUrlClient.cs +++ b/Services/Thoughts.WebAPI.Clients/ShortUrl/ShortUrlClient.cs @@ -2,50 +2,75 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; +using Thoughts.Interfaces; using Thoughts.Interfaces.Base; using Thoughts.WebAPI.Clients.Base; +using baseEntities = Thoughts.Domain.Base.Entities; namespace Thoughts.WebAPI.Clients.ShortUrl { public class ShortUrlClient: BaseClient, IShortUrlManager { - public ShortUrlClient(IConfiguration Configuration):base(Configuration,WebApiControllersPath.ShortUrl) + public ShortUrlClient(IConfiguration Configuration):base(Configuration,WebApiControllersPath.ShortUrlV1) { } - public async Task AddUrlAsync(string Url, CancellationToken Cancel = default) + public async Task AddUrlAsync(string Url, CancellationToken Cancel = default) { - var response = await PostAsync($"{WebApiControllersPath.ShortUrl}", Url); - return await response.Content.ReadAsAsync(); + if (!Regex.IsMatch(Url, @"^https?://")) + throw new FormatException("Строка адреса не имеет схемы"); + + var response = await PostAsync($"{WebApiControllersPath.ShortUrlV1}", Url); + return await response.Content.ReadAsAsync(); } public async Task DeleteUrlAsync(int Id, CancellationToken Cancel = default) { - var response = await DeleteAsync($"{WebApiControllersPath.ShortUrl}/{Id}"); + var response = await DeleteAsync($"{WebApiControllersPath.ShortUrlV1}/{Id}"); return response.IsSuccessStatusCode; } + public async Task GetAliasByIdAsync(int Id, int Length, CancellationToken Cancel = default) + { + var response = await GetAsync($"{WebApiControllersPath.ShortUrlV1}/alias/{Id}?Length={Length}"); + return response; + } + + public async Task> GetStatistic(int Id = 0, int Length=0, CancellationToken Cancel = default) + { + var response = await GetAsync>($"{WebApiControllersPath.ShortUrlV1}/getstat/{Id}?Length={Length}"); + return response; + } + public async Task GetUrlAsync(string Alias, CancellationToken Cancel = default) { - var response = await GetAsync($"{WebApiControllersPath.ShortUrl}?Alias={Alias}"); + var response = await GetAsync($"{WebApiControllersPath.ShortUrlV1}?Alias={Alias}"); return response; } public async Task GetUrlByIdAsync(int Id, CancellationToken Cancel = default) { - var response = await GetAsync($"{WebApiControllersPath.ShortUrl}/{Id}"); + var response = await GetAsync($"{WebApiControllersPath.ShortUrlV1}/{Id}"); + return response; + } + + public async Task ResetStatistic(int Id = 0, CancellationToken Cancel = default) + { + var response = await GetAsync($"{WebApiControllersPath.ShortUrlV1}/resetstat/{Id}"); return response; } public async Task UpdateUrlAsync(int Id, string Url, CancellationToken Cancel = default) { - var response = await PostAsync($"{WebApiControllersPath.ShortUrl}/{Id}", Url); + var response = await PostAsync($"{WebApiControllersPath.ShortUrlV1}/{Id}", Url); return await response.Content.ReadAsAsync(); } + } } diff --git a/Services/Thoughts.WebAPI/Controllers/ShortUrlManagerController.cs b/Services/Thoughts.WebAPI/Controllers/ShortUrlManagerController.cs deleted file mode 100644 index 4168c17..0000000 --- a/Services/Thoughts.WebAPI/Controllers/ShortUrlManagerController.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Routing; - -using Thoughts.Interfaces.Base; - -namespace Thoughts.WebAPI.Controllers -{ - [Route(WebApiControllersPath.ShortUrl)] - [ApiController] - public class ShortUrlManagerController : ControllerBase - { - private readonly IShortUrlManager _shortUrlManager; - - public ShortUrlManagerController(IShortUrlManager ShortUrlManager) - { - _shortUrlManager = ShortUrlManager; - } - - // GET: api/url?Alias=... - [HttpGet] - public async Task> GetUrl(string Alias) - { - var result = await _shortUrlManager.GetUrlAsync(Alias); - if (result is null) - return BadRequest(); - - return result; - } - - // GET: api/url/10 - [HttpGet("{Id}")] - public async Task> GetUrlById(int Id) - { - var result = await _shortUrlManager.GetUrlByIdAsync(Id); - if (result is null) - return BadRequest(); - - return result; - } - - - // POST api/url - [HttpPost] - public async Task> AddUrl([FromBody]string Url) - { - var result = await _shortUrlManager.AddUrlAsync(Url); - if (String.IsNullOrEmpty(result)) - return BadRequest(); - - return $"{result}"; - } - - // DELETE api/url/10 - [HttpDelete("{Id}")] - public async Task> DeleteUrl(int Id) - { - var result= await _shortUrlManager.DeleteUrlAsync(Id); - return result ? result : BadRequest(); - } - - // POST api/url/10 - [HttpPost("{Id}")] - public async Task> UpdateUrl(int Id, [FromBody] string Url) - { - var result=await _shortUrlManager.UpdateUrlAsync(Id, Url); - return result ? result : BadRequest(); - } - } -} diff --git a/Services/Thoughts.WebAPI/Controllers/v1/ShortUrlManagerController.cs b/Services/Thoughts.WebAPI/Controllers/v1/ShortUrlManagerController.cs new file mode 100644 index 0000000..ce1c530 --- /dev/null +++ b/Services/Thoughts.WebAPI/Controllers/v1/ShortUrlManagerController.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections; +using System.Text.RegularExpressions; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Routing; + +using Thoughts.Domain.Base.Entities; +using Thoughts.Interfaces; +using Thoughts.Interfaces.Base; + +namespace Thoughts.WebAPI.Controllers.v1 +{ + [ApiVersion("1.0")] + [Route(WebApiControllersPath.ShortUrl)] + [ApiController] + public class ShortUrlManagerController : ControllerBase + { + private readonly IShortUrlManager _shortUrlManager; + private readonly IConfiguration _configuration; + + public ShortUrlManagerController(IShortUrlManager ShortUrlManager, IConfiguration Configuration) + { + _shortUrlManager = ShortUrlManager; + _configuration = Configuration; + } + + // GET: api/v1/url?Alias=... + [HttpGet] + public async Task> GetUrl(string Alias) + { + var result = await _shortUrlManager.GetUrlAsync(Alias); + if (result is null) + return NotFound(); + + return result; + } + + // GET: api/v1/url/10 + [HttpGet("{Id}")] + public async Task> GetUrlById(int Id) + { + var result = await _shortUrlManager.GetUrlByIdAsync(Id); + if (result is null) + return NotFound(); + + return result; + } + + //GET: api/v1/url/alias/10?Length=10 + [HttpGet("alias/{Id}")] + public async Task> GetAliasById(int Id, int Length) + { + string result; + if (Length > 0) + result = await _shortUrlManager.GetAliasByIdAsync(Id, Length); + else if (int.TryParse(_configuration["ShortUrlMaxLength"], out int lengthFromConfig)) + result = await _shortUrlManager.GetAliasByIdAsync(Id, lengthFromConfig); + else + result = await _shortUrlManager.GetAliasByIdAsync(Id); + + + if (string.IsNullOrEmpty(result)) + return NotFound(); + + return AcceptedAtAction(nameof(GetUrl), new { Alias = result }, result); + } + + // POST api/v1/url + [HttpPost] + public async Task> AddUrl([FromBody] string Url) + { + if (!Regex.IsMatch(Url, @"^https?://")) + Url = "http://" + Url; + + var result = await _shortUrlManager.AddUrlAsync(Url); + if (result == 0) + return BadRequest(); + + return CreatedAtAction(nameof(GetAliasById), new { Id = result }, result); + } + + // DELETE api/v1/url/10 + [HttpDelete("{Id}")] + public async Task> DeleteUrl(int Id) + { + var result = await _shortUrlManager.DeleteUrlAsync(Id); + return result ? result : NotFound(); + } + + // POST api/v1/url/10 + [HttpPost("{Id}")] + public async Task> UpdateUrl(int Id, [FromBody] string Url) + { + var result = await _shortUrlManager.UpdateUrlAsync(Id, Url); + return result ? result : NotFound(); + } + + // GET api/v1/url/resetstat/10 + [HttpGet("resetstat/{Id}")] + public async Task> ResetStatistic(int Id) + { + var result = await _shortUrlManager.ResetStatistic(Id); + return result ? result : NotFound(); + } + + //GET: api/v1/url/getstat/10 + [HttpGet("getstat/{Id}")] + public async Task>> GetStatisticById(int Id,int Length) + { + IEnumerable result; + + if (Length > 0) + result = await _shortUrlManager.GetStatistic(Id,Length); + else if (int.TryParse(_configuration["ShortUrlMaxLength"], out int lengthFromConfig)) + result = await _shortUrlManager.GetStatistic(Id, lengthFromConfig); + else + result = await _shortUrlManager.GetStatistic(Id); + + + if (result is null || !result.Any()) + return NotFound(); + + return result.ToList(); + } + } +} diff --git a/Services/Thoughts.WebAPI/Program.cs b/Services/Thoughts.WebAPI/Program.cs index dd4e531..ccc4733 100644 --- a/Services/Thoughts.WebAPI/Program.cs +++ b/Services/Thoughts.WebAPI/Program.cs @@ -1,40 +1,62 @@ +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.Versioning; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Thoughts.DAL.Sqlite; using Thoughts.DAL.SqlServer; -using Thoughts.Interfaces.Base; +using Thoughts.Interfaces; using Thoughts.Services.InSQL; +using Thoughts.WebAPI; using Thoughts.WebAPI.Infrastructure.Extensions; var builder = WebApplication.CreateBuilder(args); var configuration = builder.Configuration; +var services = builder.Services; +services.AddControllers(); - -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +services.AddApiVersioning(opt => +{ + opt.DefaultApiVersion = new ApiVersion(1, 0); + opt.AssumeDefaultVersionWhenUnspecified = true; + opt.ReportApiVersions = true; + opt.ApiVersionReader = ApiVersionReader.Combine(new UrlSegmentApiVersionReader(), + new HeaderApiVersionReader("x-api-version"), + new MediaTypeApiVersionReader("x-api-version")); +}); +services.AddVersionedApiExplorer(setup => +{ + setup.GroupNameFormat = "'v'VVV"; + setup.SubstituteApiVersionInUrl = true; +}); +services.AddEndpointsApiExplorer(); +services.AddSwaggerGen(); +services.ConfigureOptions(); var db_type = configuration["Database"]; switch (db_type) { - default: throw new InvalidOperationException($"Òèï ÁÄ {db_type} íå ïîääåðæèâàåòñÿ"); + default: throw new InvalidOperationException($"Тип БД {db_type} не поддерживается"); case "Sqlite": - builder.Services.AddThoughtsDbSqlite(configuration.GetConnectionString("Sqlite")); + services.AddThoughtsDbSqlite(configuration.GetConnectionString("Sqlite")); break; case "SqlServer": - builder.Services.AddThoughtsDbSqlServer(configuration.GetConnectionString("SqlServer")); + services.AddThoughtsDbSqlServer(configuration.GetConnectionString("SqlServer")); break; } +services.AddScoped(); builder.Services.AddTransient(); builder.Services.AddTransient(); -builder.Services.AddControllers(); var app = builder.Build(); +var apiVersionDescriptionProvider = app.Services.GetRequiredService(); + await app.InitializeDatabase(); if (app.Environment.IsDevelopment()) diff --git a/Services/Thoughts.WebAPI/appsettings.json b/Services/Thoughts.WebAPI/appsettings.json index 798c019..b60c27e 100644 --- a/Services/Thoughts.WebAPI/appsettings.json +++ b/Services/Thoughts.WebAPI/appsettings.json @@ -14,5 +14,6 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "ShortUrlMaxLength": 5 } diff --git a/UI/Thoughts.UI.MVC/Controllers/ShortUrlController.cs b/UI/Thoughts.UI.MVC/Controllers/ShortUrlController.cs deleted file mode 100644 index 4e3341a..0000000 --- a/UI/Thoughts.UI.MVC/Controllers/ShortUrlController.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -using Thoughts.Interfaces.Base; - -namespace Thoughts.UI.MVC.Controllers -{ - [Route("url")] - public class ShortUrlController : Controller - { - private readonly IShortUrlManager _shortUrlManager; - - public ShortUrlController(IShortUrlManager ShortUrlManager) - { - _shortUrlManager = ShortUrlManager; - } - - [Route("url/{Alias}")] - [HttpGet("{Alias}")] - public async Task GetUrl(string Alias) - { - var url=await _shortUrlManager.GetUrlAsync(Alias); - if(url is null) - return NotFound(); - return Redirect(url.AbsoluteUri); - } - - [HttpPost] - public async Task AddUrl(string Url) - { - var result = await _shortUrlManager.AddUrlAsync(Url); - if (String.IsNullOrEmpty(result)) - return BadRequest(); - ShortUrlWebModel shortUrlWebModel = new() - { - Alias = result, - OriginalUrl = Url - }; - return View(shortUrlWebModel); - } - - [Route("Test")] - public IActionResult Test() - { - return View(); - } - } -} diff --git a/UI/Thoughts.UI.MVC/Controllers/UrlController.cs b/UI/Thoughts.UI.MVC/Controllers/UrlController.cs new file mode 100644 index 0000000..a408b8a --- /dev/null +++ b/UI/Thoughts.UI.MVC/Controllers/UrlController.cs @@ -0,0 +1,106 @@ +using System; + +using Microsoft.AspNetCore.Mvc; + +namespace Thoughts.UI.MVC.Controllers +{ + public class UrlController : Controller + { + private readonly IShortUrlManager _shortUrlManager; + + public UrlController(IShortUrlManager ShortUrlManager) + { + _shortUrlManager = ShortUrlManager; + } + + // GET -> https://localhost:5010/url/17D91E667026F10A16241F0784608CF1 + [Route("url/{Alias}")] + [HttpGet] + public async Task RedirectByAlias(string Alias) + { + var url = await _shortUrlManager.GetUrlAsync(Alias); + if (url is null) + return NotFound(); + return Redirect(url.AbsoluteUri); + } + + // GET -> https://localhost:5010/url/GetUrlById/10 + [Route("url/GetUrlById/{Id}")] + [HttpGet] + public async Task GetUrlById(int Id) + { + var url = await _shortUrlManager.GetUrlByIdAsync(Id); + if (url is null) + return NotFound(); + return View(url); + } + + // GET -> https://localhost:5010/url/GetAliasById/10?Length=6 + [Route("url/GetAliasById/{Id}")] + [HttpGet] + public async Task GetAliasById(int Id, int Length) + { + var alias = Length > 0 + ? await _shortUrlManager.GetAliasByIdAsync(Id, Length) + : await _shortUrlManager.GetAliasByIdAsync(Id); + if (String.IsNullOrEmpty(alias)) + return NotFound(); + var shortUrl = Url.ActionLink(action: nameof(RedirectByAlias), controller: "url", values: new { Alias = alias }); + return View(model: shortUrl); + } + + // POST -> https://localhost:5010/url/ + [Route("url")] + [HttpPost] + public async Task AddUrl(string url) + { + if (!Regex.IsMatch(url, @"^https?://")) + url = "http://" + url; + + var result = await _shortUrlManager.AddUrlAsync(url); + if (result == 0) + return BadRequest(); + var getUrl = Url.ActionLink(action: nameof(GetUrlById), controller: "url", values: new { Id = result }); + var getAlias = Url.ActionLink(action: nameof(GetAliasById), controller: "url", values: new { Id = result }); + ShortUrlWebModel shortUrlWebModel = new() + { + Id = result, + OriginalUrl = url, + GetUrl = getUrl!, + GetAlias = getAlias! + }; + return View(shortUrlWebModel); + } + + // GET -> https://localhost:5010/url/CreateUrl + [Route("url/CreateUrl")] + public async Task CreateUrl() + { + return View(); + } + + // GET -> https://localhost:5010/url/Statistic?Length=9 + [Route("url/Statistic")] + public async Task Statistic(int Length ) + { + var result = await _shortUrlManager.GetStatistic(Length: Length); + if (result is null || !result.Any()) + return BadRequest(); + + foreach (var item in result) + { + item.Alias = Url.ActionLink(action: nameof(RedirectByAlias), controller: "url", values: new { Alias = item.Alias }); + } + return View(result); + } + + // GET -> https://localhost:5010/url/ResetStatistic/10 + [Route("url/ResetStatistic/{Id}")] + public async Task ResetStatistic(int Id=0) + { + await _shortUrlManager.ResetStatistic(Id); + + return RedirectToAction("Statistic"); + } + } +} diff --git a/UI/Thoughts.UI.MVC/Program.cs b/UI/Thoughts.UI.MVC/Program.cs index 6f8c56e..88dd2a1 100644 --- a/UI/Thoughts.UI.MVC/Program.cs +++ b/UI/Thoughts.UI.MVC/Program.cs @@ -1,4 +1,3 @@ -using Thoughts.Interfaces.Base; using Thoughts.Interfaces.Base.Repositories; using Thoughts.Services.Mapping; using Thoughts.WebAPI.Clients.ShortUrl; @@ -68,10 +67,10 @@ app.UseEndpoints(endpoints => { - endpoints.MapControllerRoute( - name: "shortUrl", - pattern: "url/{Alias?}", - defaults: new { controller = "ShortUrl", action = "GetUrl" }); + //endpoints.MapControllerRoute( + // name: "shortUrl", + // pattern: "url/{Alias?}", + // defaults: new { controller = "ShortUrl", action = "GetUrl" }); endpoints.MapControllerRoute( name: "default", diff --git a/UI/Thoughts.UI.MVC/Views/Shared/Components/LeftMenu/Default.cshtml b/UI/Thoughts.UI.MVC/Views/Shared/Components/LeftMenu/Default.cshtml index 5429ae0..0002f02 100644 --- a/UI/Thoughts.UI.MVC/Views/Shared/Components/LeftMenu/Default.cshtml +++ b/UI/Thoughts.UI.MVC/Views/Shared/Components/LeftMenu/Default.cshtml @@ -5,6 +5,13 @@
  • Блог
  • Лента
  • +
  • + Короткие ссылки + +
  • Generic
  • Elements
  • diff --git a/UI/Thoughts.UI.MVC/Views/ShortUrl/AddUrl.cshtml b/UI/Thoughts.UI.MVC/Views/ShortUrl/AddUrl.cshtml deleted file mode 100644 index ed38a73..0000000 --- a/UI/Thoughts.UI.MVC/Views/ShortUrl/AddUrl.cshtml +++ /dev/null @@ -1,11 +0,0 @@ -@model ShortUrlWebModel - -@{ - ViewBag.Title = "Переход по ссылке"; -} - -
    -

    @Html.RouteLink("Короткая ссылка","shortUrl", new {Alias=@Model.Alias})

    - - Исходная ссылка: @Model.OriginalUrl -
    diff --git a/UI/Thoughts.UI.MVC/Views/ShortUrl/GetUrl.cshtml b/UI/Thoughts.UI.MVC/Views/ShortUrl/GetUrl.cshtml deleted file mode 100644 index ae8793b..0000000 --- a/UI/Thoughts.UI.MVC/Views/ShortUrl/GetUrl.cshtml +++ /dev/null @@ -1,9 +0,0 @@ -@model Uri? - -@{ - ViewBag.Title = "Переход по ссылке"; -} - -
    - @Model -
    diff --git a/UI/Thoughts.UI.MVC/Views/Url/AddUrl.cshtml b/UI/Thoughts.UI.MVC/Views/Url/AddUrl.cshtml new file mode 100644 index 0000000..3702e1f --- /dev/null +++ b/UI/Thoughts.UI.MVC/Views/Url/AddUrl.cshtml @@ -0,0 +1,12 @@ +@model ShortUrlWebModel + +@{ + ViewBag.Title = "Создание короткой ссылки"; +} + +
    +

    Id созданной ссылки: @Model.Id

    +

    Исходная ссылка: @Model.OriginalUrl

    +

    Ссылка для получения Url по Id короткой ссылки: @Model.GetUrl

    +

    Ссылка для получения короткой ссылки по Id: @Model.GetAlias

    +
    diff --git a/UI/Thoughts.UI.MVC/Views/ShortUrl/Test.cshtml b/UI/Thoughts.UI.MVC/Views/Url/CreateUrl.cshtml similarity index 72% rename from UI/Thoughts.UI.MVC/Views/ShortUrl/Test.cshtml rename to UI/Thoughts.UI.MVC/Views/Url/CreateUrl.cshtml index 6a901ed..b2b18e8 100644 --- a/UI/Thoughts.UI.MVC/Views/ShortUrl/Test.cshtml +++ b/UI/Thoughts.UI.MVC/Views/Url/CreateUrl.cshtml @@ -2,7 +2,7 @@ ViewBag.Title = "Проверка POST запроса"; } -
    + diff --git a/UI/Thoughts.UI.MVC/Views/Url/GetAliasById.cshtml b/UI/Thoughts.UI.MVC/Views/Url/GetAliasById.cshtml new file mode 100644 index 0000000..ae6ad3d --- /dev/null +++ b/UI/Thoughts.UI.MVC/Views/Url/GetAliasById.cshtml @@ -0,0 +1,8 @@ +@model string? + +@{ + ViewBag.Title = "Переход по ссылке"; +} + +

    Короткая ссылка @Model

    + diff --git a/UI/Thoughts.UI.MVC/Views/Url/GetUrlById.cshtml b/UI/Thoughts.UI.MVC/Views/Url/GetUrlById.cshtml new file mode 100644 index 0000000..3cfd2c7 --- /dev/null +++ b/UI/Thoughts.UI.MVC/Views/Url/GetUrlById.cshtml @@ -0,0 +1,8 @@ +@model Uri? + +@{ + ViewBag.Title = "Переход по ссылке"; +} + +

    Оригинальная ссылка @Model?.AbsoluteUri

    + diff --git a/UI/Thoughts.UI.MVC/Views/Url/Statistic.cshtml b/UI/Thoughts.UI.MVC/Views/Url/Statistic.cshtml new file mode 100644 index 0000000..222416b --- /dev/null +++ b/UI/Thoughts.UI.MVC/Views/Url/Statistic.cshtml @@ -0,0 +1,41 @@ +@model IEnumerable +@{ + ViewBag.Title = "Статистика использования коротких ссылок"; +} + +
    + +

    Статистика использования коротких ссылок

    + +
    + + + + + + + + + + + @foreach (var shortUrl in Model) + { + + + + + + } + +
    IdДанные ссылки
    @shortUrl.Id + СброситьОригинальная ссылка: @shortUrl.OriginalUrl
    + Короткая ссылка @shortUrl.Alias
    + Колчество вызовов: @shortUrl.Statistic
    + Дата последнего сброса статистики: @shortUrl.LastReset +
    + + Сбросить всю статистику + + +
    + diff --git a/UI/Thoughts.UI.MVC/WebModels/ShortUrlStatisticWebModel.cs b/UI/Thoughts.UI.MVC/WebModels/ShortUrlStatisticWebModel.cs new file mode 100644 index 0000000..6629237 --- /dev/null +++ b/UI/Thoughts.UI.MVC/WebModels/ShortUrlStatisticWebModel.cs @@ -0,0 +1,11 @@ +namespace Thoughts.UI.MVC.WebModels +{ + public class ShortUrlStatisticWebModel + { + public int Id { get; set; } + public string OriginalUrl { get; set; } + public string AliasUrl { get; set; } + public int Statistic { get; set; } + public DateTimeOffset LastReset { get; set; } + } +} diff --git a/UI/Thoughts.UI.MVC/WebModels/ShortUrlWebModel.cs b/UI/Thoughts.UI.MVC/WebModels/ShortUrlWebModel.cs index 45cb8ab..e7dd497 100644 --- a/UI/Thoughts.UI.MVC/WebModels/ShortUrlWebModel.cs +++ b/UI/Thoughts.UI.MVC/WebModels/ShortUrlWebModel.cs @@ -1,8 +1,10 @@ namespace Thoughts.UI.MVC.WebModels { public class ShortUrlWebModel - { - public string OriginalUrl { get; set; } - public string Alias { get; set; } + { + public int Id { get; set; } + public string OriginalUrl { get; set; } + public string GetUrl { get; set; } + public string GetAlias { get; set; } } } diff --git a/UI/Thoughts.UI.MVC/appsettings.json b/UI/Thoughts.UI.MVC/appsettings.json index 52df621..2850ac8 100644 --- a/UI/Thoughts.UI.MVC/appsettings.json +++ b/UI/Thoughts.UI.MVC/appsettings.json @@ -16,7 +16,7 @@ } }, "AllowedHosts": "*", - "WebApiUrl": "http://localhost:5001" + "WebApiUrl": "http://localhost:5001", "UploadFileOptions": { "StoredFilesPath": "c:\\files\\", "PermittedExtensions": [ ".txt", ".pdf", ".png", ".jpg", ".jpeg", ".gif", ".zip" ],