From 3877303a4099150d6e3f8bb6d4172a665f3c48a6 Mon Sep 17 00:00:00 2001 From: ozkank Date: Wed, 3 Jul 2024 17:16:20 +0300 Subject: [PATCH] Update endpoints --- .../Features/RedirectUrl.cs | 123 ++++++++---------- .../Features/ShortenUrl.cs | 15 ++- .../Database/ApiDbInitializer.cs | 70 ++++++++++ .../20240702143044_Init.Designer.cs | 60 +++++++++ .../Migrations/20240702143044_Init.cs | 43 ++++++ .../ApplicationDbContextModelSnapshot.cs | 57 ++++++++ UrlShortener.ApiService/Program.cs | 8 +- .../Properties/launchSettings.json | 2 +- .../UrlShortener.ApiService.csproj | 4 + .../Properties/launchSettings.json | 4 +- 10 files changed, 309 insertions(+), 77 deletions(-) create mode 100644 UrlShortener.ApiService/Infrastructure/Database/ApiDbInitializer.cs create mode 100644 UrlShortener.ApiService/Migrations/20240702143044_Init.Designer.cs create mode 100644 UrlShortener.ApiService/Migrations/20240702143044_Init.cs create mode 100644 UrlShortener.ApiService/Migrations/ApplicationDbContextModelSnapshot.cs diff --git a/UrlShortener.ApiService/Features/RedirectUrl.cs b/UrlShortener.ApiService/Features/RedirectUrl.cs index 06d2c1e..da59fd1 100644 --- a/UrlShortener.ApiService/Features/RedirectUrl.cs +++ b/UrlShortener.ApiService/Features/RedirectUrl.cs @@ -1,79 +1,68 @@ -//using Azure.Core; -//using Carter; -//using FluentValidation; -//using MediatR; -//using Microsoft.EntityFrameworkCore; -//using UrlShortener.ApiService.Infrastructure.Database; -//using UrlShortener.ApiService.Shared; +using Azure.Core; +using Carter; +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; +using UrlShortener.ApiService.Infrastructure.Database; +using UrlShortener.ApiService.Shared; -//namespace UrlShortener.ApiService.Features -//{ -// public class RedirectUrl -// { -// public class Query : IRequest> -// { -// public string Code { get; set; } = string.Empty; -// } +namespace UrlShortener.ApiService.Features +{ + public class RedirectUrl + { + public class Query : IRequest> + { + public string Code { get; set; } = string.Empty; + } -// internal sealed class Handler : IRequestHandler> -// { -// private readonly ApplicationDbContext _context; -// public const int NumberOfCharsInShortLink = 7; -// private const string Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + internal sealed class Handler : IRequestHandler> + { + private readonly ApplicationDbContext _dbContext; + public const int NumberOfCharsInShortLink = 7; + private const string Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; -// private readonly Random _random = new(); + private readonly Random _random = new(); -// public Handler(ApplicationDbContext context) -// { -// _context = context; -// } + public Handler(ApplicationDbContext dbContext) + { + _dbContext = dbContext; + } -// public async Task> Handle(Query request, CancellationToken cancellationToken) -// { -// var item = await _context -// .ShortenedUrls -// .Where(s => s.Code == request.Code) -// .Select(x => new RedirectUrlResponse -// { -// LongUrl = x.LongUrl, -// }) -// .FirstOrDefaultAsync(cancellationToken); + public async Task> Handle(Query request, CancellationToken cancellationToken) + { + var shortenedUrl = await _dbContext.ShortenedUrls + .FirstOrDefaultAsync(s => s.Code == request.Code); -// if (item is null) -// { -// return Result.Failure(new Error( -// "RedirectUrlResponse.Null", -// "The longUrl with the specified shortUrl was not found")); -// } + if (shortenedUrl is null) + { + return Result.Failure(new Error( + "RedirectUrlResponse.Null", + "The longUrl with the specified shortUrl was not found")); + } -// return item; -// } -// } + return shortenedUrl.LongUrl; + } + } -// } + } -// public class RedirectUrlEndpoint : ICarterModule -// { -// public void AddRoutes(IEndpointRouteBuilder app) -// { -// app.MapGet("api/redirect/{shortUrl}", async (string shortUrl, ISender sender) => -// { -// var query = new RedirectUrl.Query { Code = shortUrl }; + public class RedirectUrlEndpoint : ICarterModule + { + public void AddRoutes(IEndpointRouteBuilder app) + { + app.MapGet("api/{code}", async (string code, ISender sender) => + { + var query = new RedirectUrl.Query { Code = code }; -// var result = await sender.Send(query); + var result = await sender.Send(query); -// if (result.IsFailure) -// { -// return Results.NotFound(result.Error); -// } + if (result.IsFailure) + { + return Results.NotFound(result.Error); + } -// return Results.Ok(result.Value); -// }); -// } -// } - -// public class RedirectUrlResponse -// { -// public string LongUrl { get; set; } = string.Empty; -// } -//} + return Results.Redirect(result.Value); + }); + } + } +} diff --git a/UrlShortener.ApiService/Features/ShortenUrl.cs b/UrlShortener.ApiService/Features/ShortenUrl.cs index 8359864..5dd6dcd 100644 --- a/UrlShortener.ApiService/Features/ShortenUrl.cs +++ b/UrlShortener.ApiService/Features/ShortenUrl.cs @@ -1,9 +1,7 @@ using Carter; using FluentValidation; using MediatR; -using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; -using System; using UrlShortener.ApiService.Domain; using UrlShortener.ApiService.Infrastructure.Database; using UrlShortener.ApiService.Shared; @@ -32,14 +30,16 @@ internal sealed class Handler : IRequestHandler> public const int NumberOfCharsInShortLink = 7; private const string Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; private readonly Random _random = new(); + private readonly IHttpContextAccessor _httpContextAccessor; - public Handler(ApplicationDbContext dbContext, IValidator validator) + public Handler(ApplicationDbContext dbContext, IValidator validator, IHttpContextAccessor httpContextAccessor) { _dbContext = dbContext; _validator = validator; + _httpContextAccessor = httpContextAccessor; } - public async Task> Handle(Command request, CancellationToken cancellationToken) + public async Task> Handle(Command request, CancellationToken cancellationToken) { var validationResult = _validator.Validate(request); if (!validationResult.IsValid) @@ -51,12 +51,15 @@ public async Task> Handle(Command request, CancellationToken can var code = await GenerateUniqueCode(); + var httpContext = _httpContextAccessor.HttpContext; + var shortUrl = $"{httpContext.Request.Scheme}://{httpContext.Request.Host}/api/{code}"; + var shortenedUrl = new ShortenedUrl { Id = Guid.NewGuid(), LongUrl = request.LongUrl, Code = code, - //ShortUrl = $"{httpContext.Request.Scheme}://{httpContext.Request.Host}/api/{code}", + ShortUrl = shortUrl, CreatedOnUtc = DateTime.UtcNow }; @@ -95,7 +98,7 @@ public class ShortenUrlEndpoint : ICarterModule { public void AddRoutes(IEndpointRouteBuilder app) { - app.MapPost("api/articles", async (ShortenUrl.Command request, ISender sender) => + app.MapPost("api/shorten", async (ShortenUrl.Command request, ISender sender) => { var result = await sender.Send(request); diff --git a/UrlShortener.ApiService/Infrastructure/Database/ApiDbInitializer.cs b/UrlShortener.ApiService/Infrastructure/Database/ApiDbInitializer.cs new file mode 100644 index 0000000..acd6263 --- /dev/null +++ b/UrlShortener.ApiService/Infrastructure/Database/ApiDbInitializer.cs @@ -0,0 +1,70 @@ +using Microsoft.EntityFrameworkCore.Storage; +using System.Diagnostics; +using System.ComponentModel; +using System.Diagnostics; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Trace; + +namespace UrlShortener.ApiService.Infrastructure.Database +{ + public class ApiDbInitializer( + IServiceProvider serviceProvider, + IHostApplicationLifetime hostApplicationLifetime) : BackgroundService + { + public const string ActivitySourceName = "Migrations"; + private static readonly ActivitySource s_activitySource = new(ActivitySourceName); + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + using var activity = s_activitySource.StartActivity("Migrating database", ActivityKind.Client); + + try + { + using var scope = serviceProvider.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + + await EnsureDatabaseAsync(dbContext, cancellationToken); + await RunMigrationAsync(dbContext, cancellationToken); + } + catch (Exception ex) + { + activity?.RecordException(ex); + throw; + } + + hostApplicationLifetime.StopApplication(); + } + + private static async Task EnsureDatabaseAsync(ApplicationDbContext dbContext, CancellationToken cancellationToken) + { + var dbCreator = dbContext.GetService(); + + var strategy = dbContext.Database.CreateExecutionStrategy(); + await strategy.ExecuteAsync(async () => + { + // Create the database if it does not exist. + // Do this first so there is then a database to start a transaction against. + if (!await dbCreator.ExistsAsync(cancellationToken)) + { + await dbCreator.CreateAsync(cancellationToken); + } + }); + } + + private static async Task RunMigrationAsync(ApplicationDbContext dbContext, CancellationToken cancellationToken) + { + var strategy = dbContext.Database.CreateExecutionStrategy(); + await strategy.ExecuteAsync(async () => + { + // Run migration in a transaction to avoid partial migration if it fails. + await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken); + await dbContext.Database.MigrateAsync(cancellationToken); + await transaction.CommitAsync(cancellationToken); + }); + } + } +} diff --git a/UrlShortener.ApiService/Migrations/20240702143044_Init.Designer.cs b/UrlShortener.ApiService/Migrations/20240702143044_Init.Designer.cs new file mode 100644 index 0000000..be285a6 --- /dev/null +++ b/UrlShortener.ApiService/Migrations/20240702143044_Init.Designer.cs @@ -0,0 +1,60 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using UrlShortener.ApiService.Infrastructure.Database; + +#nullable disable + +namespace UrlShortener.ApiService.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20240702143044_Init")] + partial class Init + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("UrlShortener.ApiService.Domain.ShortenedUrl", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("nvarchar(7)"); + + b.Property("CreatedOnUtc") + .HasColumnType("datetime2"); + + b.Property("LongUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShortUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("ShortenedUrls"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/UrlShortener.ApiService/Migrations/20240702143044_Init.cs b/UrlShortener.ApiService/Migrations/20240702143044_Init.cs new file mode 100644 index 0000000..28bebe6 --- /dev/null +++ b/UrlShortener.ApiService/Migrations/20240702143044_Init.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace UrlShortener.ApiService.Migrations +{ + /// + public partial class Init : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ShortenedUrls", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + LongUrl = table.Column(type: "nvarchar(max)", nullable: false), + ShortUrl = table.Column(type: "nvarchar(max)", nullable: false), + Code = table.Column(type: "nvarchar(7)", maxLength: 7, nullable: false), + CreatedOnUtc = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ShortenedUrls", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_ShortenedUrls_Code", + table: "ShortenedUrls", + column: "Code", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ShortenedUrls"); + } + } +} diff --git a/UrlShortener.ApiService/Migrations/ApplicationDbContextModelSnapshot.cs b/UrlShortener.ApiService/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..ed2cf82 --- /dev/null +++ b/UrlShortener.ApiService/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,57 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using UrlShortener.ApiService.Infrastructure.Database; + +#nullable disable + +namespace UrlShortener.ApiService.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("UrlShortener.ApiService.Domain.ShortenedUrl", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("nvarchar(7)"); + + b.Property("CreatedOnUtc") + .HasColumnType("datetime2"); + + b.Property("LongUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ShortUrl") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("ShortenedUrls"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/UrlShortener.ApiService/Program.cs b/UrlShortener.ApiService/Program.cs index a8ffadf..0dfa8d9 100644 --- a/UrlShortener.ApiService/Program.cs +++ b/UrlShortener.ApiService/Program.cs @@ -22,13 +22,19 @@ builder.Services.AddEndpoints(typeof(Program).Assembly); - +//builder.Services.AddHostedService(); builder.Services.AddMediatR(config => config.RegisterServicesFromAssembly(typeof(Program).Assembly)); builder.Services.AddCarter(); builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly); +builder.Services.AddHttpContextAccessor(); + + +builder.Services.AddOpenTelemetry() + .WithTracing(tracing => tracing.AddSource(ApiDbInitializer.ActivitySourceName)); + var app = builder.Build(); // Configure the HTTP request pipeline. diff --git a/UrlShortener.ApiService/Properties/launchSettings.json b/UrlShortener.ApiService/Properties/launchSettings.json index d7a8670..c0e46d4 100644 --- a/UrlShortener.ApiService/Properties/launchSettings.json +++ b/UrlShortener.ApiService/Properties/launchSettings.json @@ -15,7 +15,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "weatherforecast", + "launchUrl": "swagger", "applicationUrl": "https://localhost:7549;http://localhost:5385", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/UrlShortener.ApiService/UrlShortener.ApiService.csproj b/UrlShortener.ApiService/UrlShortener.ApiService.csproj index 6e34e76..79c0d97 100644 --- a/UrlShortener.ApiService/UrlShortener.ApiService.csproj +++ b/UrlShortener.ApiService/UrlShortener.ApiService.csproj @@ -17,6 +17,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive +