From a6cb2015f49a38ae0b704a97822c35fd27bf9689 Mon Sep 17 00:00:00 2001 From: Hernan Demo Date: Thu, 14 Nov 2019 15:29:39 -0300 Subject: [PATCH] First commit for: code-test solution. --- .gitignore | 32 +++ Install.md | 19 ++ .../CrossCutting/CodeUtilities.cs | 25 ++ .../Exceptions/BadRequestException.cs | 20 ++ .../Exceptions/ConflictException.cs | 20 ++ .../Exceptions/NotFoundException.cs | 20 ++ .../UnprocessableEntityException.cs | 20 ++ .../UrlShortener.CrossCutting.csproj | 7 + .../Bootstrap/Filters/ExceptionFilter.cs | 23 ++ .../Filters/ExceptionFilterFactory.cs | 34 +++ .../Bootstrap/MappingProfile.cs | 17 ++ .../Controllers/ShortUrlController.cs | 47 ++++ src/UrlShortener/UrlShortener.Api/Program.cs | 26 ++ .../Properties/launchSettings.json | 30 +++ src/UrlShortener/UrlShortener.Api/Startup.cs | 78 ++++++ .../UrlShortener.Api/UrlShortener.Api.csproj | 23 ++ .../appsettings.Development.json | 9 + .../UrlShortener.Api/appsettings.json | 10 + .../UrlShortener.Infra/DB/urlshortener.db | 0 .../UrlShortener.Infra/Data/Repository.cs | 65 +++++ .../Data/UrlShortenerContext.cs | 34 +++ .../UrlShortener.Infra.csproj | 19 ++ .../Commands/GenerateCodeCommandTest.cs | 67 +++++ .../Commands/ShortenUrlUsageCommandTest.cs | 48 ++++ .../UrlShortenerCreatorServiceTest.cs | 119 +++++++++ .../Services/UrlShortenerServiceTest.cs | 229 ++++++++++++++++++ .../UrlShortener.Test.csproj | 21 ++ .../Validators/CodeValidatorTest.cs | 58 +++++ src/UrlShortener/UrlShortener.sln | 49 ++++ .../Commands/GenerateCodeCommand.cs | 13 + .../Commands/ShortenUrlUsageCommand.cs | 17 ++ .../Dto/ShortUrCreatedlDto.cs | 8 + .../Urlshortener.Domain/Dto/ShortUrlDto.cs | 12 + .../Urlshortener.Domain/Dto/UrlStatDto.cs | 34 +++ .../Entities/BaseEntity.cs | 9 + .../Urlshortener.Domain/Entities/ShortUrl.cs | 17 ++ .../Commands/IGenerateCodeCommand.cs | 10 + .../Commands/IShortenUrlUsageCommand.cs | 11 + .../Creators/IUrlShortenerCreatorService.cs | 13 + .../Interfaces/ICommandWithResult.cs | 11 + .../Interfaces/ICommandWithoutResult.cs | 11 + .../Interfaces/IRepository.cs | 20 ++ .../Interfaces/IUrlShortenerService.cs | 18 ++ .../Interfaces/Validators/ICodeValidator.cs | 10 + .../UrlShortenerBaseCreatorService.cs | 37 +++ .../Creators/UrlShortenerCreatorService.cs | 49 ++++ .../Services/UrlShortenerService.cs | 94 +++++++ .../UrlShortener.Domain.csproj | 15 ++ .../Validators/CodeValidator.cs | 13 + 49 files changed, 1591 insertions(+) create mode 100644 .gitignore create mode 100644 Install.md create mode 100644 src/UrlShortener/CrossCutting/CodeUtilities.cs create mode 100644 src/UrlShortener/CrossCutting/Exceptions/BadRequestException.cs create mode 100644 src/UrlShortener/CrossCutting/Exceptions/ConflictException.cs create mode 100644 src/UrlShortener/CrossCutting/Exceptions/NotFoundException.cs create mode 100644 src/UrlShortener/CrossCutting/Exceptions/UnprocessableEntityException.cs create mode 100644 src/UrlShortener/CrossCutting/UrlShortener.CrossCutting.csproj create mode 100644 src/UrlShortener/UrlShortener.Api/Bootstrap/Filters/ExceptionFilter.cs create mode 100644 src/UrlShortener/UrlShortener.Api/Bootstrap/Filters/ExceptionFilterFactory.cs create mode 100644 src/UrlShortener/UrlShortener.Api/Bootstrap/MappingProfile.cs create mode 100644 src/UrlShortener/UrlShortener.Api/Controllers/ShortUrlController.cs create mode 100644 src/UrlShortener/UrlShortener.Api/Program.cs create mode 100644 src/UrlShortener/UrlShortener.Api/Properties/launchSettings.json create mode 100644 src/UrlShortener/UrlShortener.Api/Startup.cs create mode 100644 src/UrlShortener/UrlShortener.Api/UrlShortener.Api.csproj create mode 100644 src/UrlShortener/UrlShortener.Api/appsettings.Development.json create mode 100644 src/UrlShortener/UrlShortener.Api/appsettings.json create mode 100644 src/UrlShortener/UrlShortener.Infra/DB/urlshortener.db create mode 100644 src/UrlShortener/UrlShortener.Infra/Data/Repository.cs create mode 100644 src/UrlShortener/UrlShortener.Infra/Data/UrlShortenerContext.cs create mode 100644 src/UrlShortener/UrlShortener.Infra/UrlShortener.Infra.csproj create mode 100644 src/UrlShortener/UrlShortener.Test/Commands/GenerateCodeCommandTest.cs create mode 100644 src/UrlShortener/UrlShortener.Test/Commands/ShortenUrlUsageCommandTest.cs create mode 100644 src/UrlShortener/UrlShortener.Test/Services/UrlShortenerCreatorServiceTest.cs create mode 100644 src/UrlShortener/UrlShortener.Test/Services/UrlShortenerServiceTest.cs create mode 100644 src/UrlShortener/UrlShortener.Test/UrlShortener.Test.csproj create mode 100644 src/UrlShortener/UrlShortener.Test/Validators/CodeValidatorTest.cs create mode 100644 src/UrlShortener/UrlShortener.sln create mode 100644 src/UrlShortener/Urlshortener.Domain/Commands/GenerateCodeCommand.cs create mode 100644 src/UrlShortener/Urlshortener.Domain/Commands/ShortenUrlUsageCommand.cs create mode 100644 src/UrlShortener/Urlshortener.Domain/Dto/ShortUrCreatedlDto.cs create mode 100644 src/UrlShortener/Urlshortener.Domain/Dto/ShortUrlDto.cs create mode 100644 src/UrlShortener/Urlshortener.Domain/Dto/UrlStatDto.cs create mode 100644 src/UrlShortener/Urlshortener.Domain/Entities/BaseEntity.cs create mode 100644 src/UrlShortener/Urlshortener.Domain/Entities/ShortUrl.cs create mode 100644 src/UrlShortener/Urlshortener.Domain/Interfaces/Commands/IGenerateCodeCommand.cs create mode 100644 src/UrlShortener/Urlshortener.Domain/Interfaces/Commands/IShortenUrlUsageCommand.cs create mode 100644 src/UrlShortener/Urlshortener.Domain/Interfaces/Creators/IUrlShortenerCreatorService.cs create mode 100644 src/UrlShortener/Urlshortener.Domain/Interfaces/ICommandWithResult.cs create mode 100644 src/UrlShortener/Urlshortener.Domain/Interfaces/ICommandWithoutResult.cs create mode 100644 src/UrlShortener/Urlshortener.Domain/Interfaces/IRepository.cs create mode 100644 src/UrlShortener/Urlshortener.Domain/Interfaces/IUrlShortenerService.cs create mode 100644 src/UrlShortener/Urlshortener.Domain/Interfaces/Validators/ICodeValidator.cs create mode 100644 src/UrlShortener/Urlshortener.Domain/Services/Creators/UrlShortenerBaseCreatorService.cs create mode 100644 src/UrlShortener/Urlshortener.Domain/Services/Creators/UrlShortenerCreatorService.cs create mode 100644 src/UrlShortener/Urlshortener.Domain/Services/UrlShortenerService.cs create mode 100644 src/UrlShortener/Urlshortener.Domain/UrlShortener.Domain.csproj create mode 100644 src/UrlShortener/Urlshortener.Domain/Validators/CodeValidator.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..07ba847 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +*.swp +*.*~ +project.lock.json +.DS_Store +*.pyc + +# Visual Studio Code +.vscode + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ +msbuild.log +msbuild.err +msbuild.wrn + +# Visual Studio 2015 +.vs/ \ No newline at end of file diff --git a/Install.md b/Install.md new file mode 100644 index 0000000..316661f --- /dev/null +++ b/Install.md @@ -0,0 +1,19 @@ +# Installation Guide +------------------------------------------------------------------------- + +### Required Software + + - Visual Studio 2019. [Community version](https://visualstudio.microsoft.com/vs/community/) + - Net Core 3. + + +### Installation + + - Clone the code-test project. + - Open the solution file => src\UrlShortener\UrlShortener.sln and Run! + +### DataBase + - The project use SqlLite. + - After runnning for the first time, you can find the sqlLite database file under: src\UrlShortener\UrlShortener.Api\UrlShortener.db + + diff --git a/src/UrlShortener/CrossCutting/CodeUtilities.cs b/src/UrlShortener/CrossCutting/CodeUtilities.cs new file mode 100644 index 0000000..ffc4b7e --- /dev/null +++ b/src/UrlShortener/CrossCutting/CodeUtilities.cs @@ -0,0 +1,25 @@ +using System; +using System.Linq; + +namespace UrlShortener.CrossCutting +{ + public static class CodeUtilities + { + private static string source = "abcdefghijklmnopqrstuvwxyz0123456789"; + private static readonly Random random; + + static CodeUtilities() + { + random = new Random(); + } + + public static string Generate(int lengh) => CreateRandomWord(lengh); + + private static string CreateRandomWord(int length) + { + return new string(Enumerable.Range(1, length) + .Select(_ => source[new Random().Next(source.Length)]) + .ToArray()); + } + } +} diff --git a/src/UrlShortener/CrossCutting/Exceptions/BadRequestException.cs b/src/UrlShortener/CrossCutting/Exceptions/BadRequestException.cs new file mode 100644 index 0000000..25dc339 --- /dev/null +++ b/src/UrlShortener/CrossCutting/Exceptions/BadRequestException.cs @@ -0,0 +1,20 @@ +using System; + +namespace UrlShortener.CrossCutting.Exceptions +{ + public class BadRequestException : Exception + { + public BadRequestException() + { + } + + public BadRequestException(string message) : base(message) + { + } + + public BadRequestException(string message, Exception inner) + : base(message, inner) + { + } + } +} diff --git a/src/UrlShortener/CrossCutting/Exceptions/ConflictException.cs b/src/UrlShortener/CrossCutting/Exceptions/ConflictException.cs new file mode 100644 index 0000000..47ce7fa --- /dev/null +++ b/src/UrlShortener/CrossCutting/Exceptions/ConflictException.cs @@ -0,0 +1,20 @@ +using System; + +namespace UrlShortener.CrossCutting.Exceptions +{ + public class ConflictException : Exception + { + public ConflictException() + { + } + + public ConflictException(string message) : base(message) + { + } + + public ConflictException(string message, Exception inner) + : base(message, inner) + { + } + } +} diff --git a/src/UrlShortener/CrossCutting/Exceptions/NotFoundException.cs b/src/UrlShortener/CrossCutting/Exceptions/NotFoundException.cs new file mode 100644 index 0000000..6686197 --- /dev/null +++ b/src/UrlShortener/CrossCutting/Exceptions/NotFoundException.cs @@ -0,0 +1,20 @@ +using System; + +namespace UrlShortener.CrossCutting.Exceptions +{ + public class NotFoundException : Exception + { + public NotFoundException() + { + } + + public NotFoundException(string message) : base(message) + { + } + + public NotFoundException(string message, Exception inner) + : base(message, inner) + { + } + } +} diff --git a/src/UrlShortener/CrossCutting/Exceptions/UnprocessableEntityException.cs b/src/UrlShortener/CrossCutting/Exceptions/UnprocessableEntityException.cs new file mode 100644 index 0000000..1e06b3a --- /dev/null +++ b/src/UrlShortener/CrossCutting/Exceptions/UnprocessableEntityException.cs @@ -0,0 +1,20 @@ +using System; + +namespace UrlShortener.CrossCutting.Exceptions +{ + public class UnprocessableEntityException : Exception + { + public UnprocessableEntityException() + { + } + + public UnprocessableEntityException(string message) : base(message) + { + } + + public UnprocessableEntityException(string message, Exception inner) + : base(message, inner) + { + } + } +} diff --git a/src/UrlShortener/CrossCutting/UrlShortener.CrossCutting.csproj b/src/UrlShortener/CrossCutting/UrlShortener.CrossCutting.csproj new file mode 100644 index 0000000..ea83d29 --- /dev/null +++ b/src/UrlShortener/CrossCutting/UrlShortener.CrossCutting.csproj @@ -0,0 +1,7 @@ + + + + netcoreapp3.0 + + + diff --git a/src/UrlShortener/UrlShortener.Api/Bootstrap/Filters/ExceptionFilter.cs b/src/UrlShortener/UrlShortener.Api/Bootstrap/Filters/ExceptionFilter.cs new file mode 100644 index 0000000..8fc6781 --- /dev/null +++ b/src/UrlShortener/UrlShortener.Api/Bootstrap/Filters/ExceptionFilter.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace UrlShortener.Api.Bootstrap.Filters +{ + public class ExceptionFilter : ExceptionFilterAttribute + { + public override void OnException(ExceptionContext context) + { + var message = context.Exception.GetBaseException().Message; + + var exceptionName = context.Exception.GetType().Name; + + var exception = ExceptionFilterFactory.Get(exceptionName); + + context.HttpContext.Response.StatusCode = exception; + + context.Result = new JsonResult(message); + + base.OnException(context); + } + } +} diff --git a/src/UrlShortener/UrlShortener.Api/Bootstrap/Filters/ExceptionFilterFactory.cs b/src/UrlShortener/UrlShortener.Api/Bootstrap/Filters/ExceptionFilterFactory.cs new file mode 100644 index 0000000..25a3448 --- /dev/null +++ b/src/UrlShortener/UrlShortener.Api/Bootstrap/Filters/ExceptionFilterFactory.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Net; + +namespace UrlShortener.Api.Bootstrap.Filters +{ + public class ExceptionFilterFactory + { + private static IReadOnlyDictionary Exceptions; + + static ExceptionFilterFactory() + { + Exceptions = new Dictionary + { + {"BadRequestException" , (int)HttpStatusCode.BadRequest}, + {"ConflictException", (int)HttpStatusCode.Conflict}, + {"UnprocessableEntityException", (int)HttpStatusCode.UnprocessableEntity}, + {"NotFoundException", (int)HttpStatusCode.NotFound}, + }; + } + + public static int Get(string name) + { + int code; + + if (!Exceptions.TryGetValue(name, out code)) + { + throw new ArgumentOutOfRangeException($"The name {name} is not mapped to any collection in the exception filter configuration"); + } + + return code; + } + } +} diff --git a/src/UrlShortener/UrlShortener.Api/Bootstrap/MappingProfile.cs b/src/UrlShortener/UrlShortener.Api/Bootstrap/MappingProfile.cs new file mode 100644 index 0000000..0c46bda --- /dev/null +++ b/src/UrlShortener/UrlShortener.Api/Bootstrap/MappingProfile.cs @@ -0,0 +1,17 @@ +using AutoMapper; +using UrlShortener.Domain.Dto; +using UrlShortener.Domain.Entities; + +namespace UrlShortener.Api.Bootstrap +{ + public class MappingProfile : Profile + { + public MappingProfile() + { + CreateMap(); + CreateMap(); + + CreateMap(); + } + } +} diff --git a/src/UrlShortener/UrlShortener.Api/Controllers/ShortUrlController.cs b/src/UrlShortener/UrlShortener.Api/Controllers/ShortUrlController.cs new file mode 100644 index 0000000..7e3f75b --- /dev/null +++ b/src/UrlShortener/UrlShortener.Api/Controllers/ShortUrlController.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using UrlShortener.Domain.Dto; +using UrlShortener.Domain.Interfaces; + +namespace UrlShortener.Api.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class ShortUrlController : ControllerBase + { + private readonly IUrlShortenerService urlShortenerService; + public ShortUrlController(IUrlShortenerService urlShortenerService) + { + this.urlShortenerService = urlShortenerService; + } + + [HttpPost] + public IActionResult Post(ShortUrlDto shortUrlDto) + { + if (string.IsNullOrEmpty(shortUrlDto.Url)) + { + return BadRequest("Url is not present"); + } + + var shortUrlCreated = this.urlShortenerService.Add(shortUrlDto); + + return Created(string.Empty, shortUrlCreated); + } + + [HttpGet("{code}/stats")] + public async Task GetStatByCode(string code) + { + var result = await this.urlShortenerService.GetStatByCode(code); + + return Ok(result); + } + + [HttpGet("{code}")] + public async Task GetUrlByCode(string code) + { + var url = await this.urlShortenerService.GetUrlByCode(code); + + return Redirect(url); + } + } +} \ No newline at end of file diff --git a/src/UrlShortener/UrlShortener.Api/Program.cs b/src/UrlShortener/UrlShortener.Api/Program.cs new file mode 100644 index 0000000..85e4105 --- /dev/null +++ b/src/UrlShortener/UrlShortener.Api/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace UrlShortener.Api +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/src/UrlShortener/UrlShortener.Api/Properties/launchSettings.json b/src/UrlShortener/UrlShortener.Api/Properties/launchSettings.json new file mode 100644 index 0000000..8ff1d54 --- /dev/null +++ b/src/UrlShortener/UrlShortener.Api/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:1651", + "sslPort": 44398 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "UrlShortener.Api": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/UrlShortener/UrlShortener.Api/Startup.cs b/src/UrlShortener/UrlShortener.Api/Startup.cs new file mode 100644 index 0000000..55208b9 --- /dev/null +++ b/src/UrlShortener/UrlShortener.Api/Startup.cs @@ -0,0 +1,78 @@ +using AutoMapper; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using UrlShortener.Api.Bootstrap.Filters; +using UrlShortener.Domain.Commands; +using UrlShortener.Domain.Interfaces; +using UrlShortener.Domain.Interfaces.Commands; +using UrlShortener.Domain.Interfaces.Creators; +using UrlShortener.Domain.Interfaces.Validators; +using UrlShortener.Domain.Services; +using UrlShortener.Domain.Services.Creators; +using UrlShortener.Domain.Validators; +using UrlShortener.Infra.Data; + +namespace UrlShortener.Api +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + + using (var db = new UrlShortenerContext()) + { + db.Database.EnsureCreated(); + } + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddTransient(typeof(UrlShortenerContext)); + services.AddTransient(typeof(IRepository<>), typeof(Repository<>)); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + services.AddAutoMapper(typeof(Startup)); + + services + .AddControllers(option => + { + option.Filters.Add(new ExceptionFilter()); + }) + .AddJsonOptions(option => + { + option.JsonSerializerOptions.IgnoreNullValues = true; + }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} diff --git a/src/UrlShortener/UrlShortener.Api/UrlShortener.Api.csproj b/src/UrlShortener/UrlShortener.Api/UrlShortener.Api.csproj new file mode 100644 index 0000000..fa7a60d --- /dev/null +++ b/src/UrlShortener/UrlShortener.Api/UrlShortener.Api.csproj @@ -0,0 +1,23 @@ + + + + netcoreapp3.0 + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/src/UrlShortener/UrlShortener.Api/appsettings.Development.json b/src/UrlShortener/UrlShortener.Api/appsettings.Development.json new file mode 100644 index 0000000..e203e94 --- /dev/null +++ b/src/UrlShortener/UrlShortener.Api/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/src/UrlShortener/UrlShortener.Api/appsettings.json b/src/UrlShortener/UrlShortener.Api/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/src/UrlShortener/UrlShortener.Api/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/src/UrlShortener/UrlShortener.Infra/DB/urlshortener.db b/src/UrlShortener/UrlShortener.Infra/DB/urlshortener.db new file mode 100644 index 0000000..e69de29 diff --git a/src/UrlShortener/UrlShortener.Infra/Data/Repository.cs b/src/UrlShortener/UrlShortener.Infra/Data/Repository.cs new file mode 100644 index 0000000..fe07291 --- /dev/null +++ b/src/UrlShortener/UrlShortener.Infra/Data/Repository.cs @@ -0,0 +1,65 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; +using UrlShortener.Domain.Entities; +using UrlShortener.Domain.Interfaces; + +namespace UrlShortener.Infra.Data +{ + public class Repository : IRepository where T : BaseEntity + { + private readonly UrlShortenerContext context; + private readonly DbSet dbset; + + public Repository(UrlShortenerContext context) + { + this.context = context; + this.dbset = this.context.Set(); + } + + public void Add(T entity) + { + this.dbset.Add(entity); + + this.context.SaveChanges(); + } + + public void Delete(T entity) + { + dbset.Remove(entity); + } + + public T GetById(string id) + { + return dbset.FirstOrDefault(x => x.Id.Equals(id)); + } + + public async Task GetByIdAsync(string id) + { + return await dbset.FirstOrDefaultAsync(x => x.Id.Equals(id)); + } + + public async Task> ListAsync(Expression> filter = null) + { + IQueryable query = dbset; + + if (filter != null) + { + query = query.Where(filter); + } + + return await query.ToListAsync(); + } + + public void Update(T entity) + { + this.dbset.Update(entity); + + this.context.SaveChanges(); + } + } +} diff --git a/src/UrlShortener/UrlShortener.Infra/Data/UrlShortenerContext.cs b/src/UrlShortener/UrlShortener.Infra/Data/UrlShortenerContext.cs new file mode 100644 index 0000000..0b650ed --- /dev/null +++ b/src/UrlShortener/UrlShortener.Infra/Data/UrlShortenerContext.cs @@ -0,0 +1,34 @@ +using Microsoft.EntityFrameworkCore; +using System.Reflection; +using UrlShortener.Domain.Entities; + +namespace UrlShortener.Infra.Data +{ + public class UrlShortenerContext : DbContext + { + public DbSet ShortUrls { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSqlite("Filename=UrlShortener.db", options => + { + options.MigrationsAssembly(Assembly.GetExecutingAssembly().FullName); + }); + + base.OnConfiguring(optionsBuilder); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().ToTable("URLShortener", "url"); + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.HasIndex(e => e.Code).IsUnique(); + entity.Property(e => e.CreatedAt).HasDefaultValueSql("CURRENT_TIMESTAMP"); + }); + + base.OnModelCreating(modelBuilder); + } + } +} diff --git a/src/UrlShortener/UrlShortener.Infra/UrlShortener.Infra.csproj b/src/UrlShortener/UrlShortener.Infra/UrlShortener.Infra.csproj new file mode 100644 index 0000000..a02d1ff --- /dev/null +++ b/src/UrlShortener/UrlShortener.Infra/UrlShortener.Infra.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp3.0 + + + + + + + + + + + + + + + diff --git a/src/UrlShortener/UrlShortener.Test/Commands/GenerateCodeCommandTest.cs b/src/UrlShortener/UrlShortener.Test/Commands/GenerateCodeCommandTest.cs new file mode 100644 index 0000000..24fd045 --- /dev/null +++ b/src/UrlShortener/UrlShortener.Test/Commands/GenerateCodeCommandTest.cs @@ -0,0 +1,67 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Text; +using UrlShortener.Domain.Commands; +using Xunit; + +namespace UrlShortener.Test.Commands +{ + public class GenerateCodeCommandTest + { + [Fact] + public void ShouldGenerateValidLengthCode() + { + var length = 6; + var command = new GenerateCodeCommand(); + + var code = command.Execute(length); + + Assert.Equal(length, code.Length); + } + + [Fact] + public void ShouldGenerateInvalidValidLengthCode() + { + var length = 6; + var invalidLength = 8; + + var command = new GenerateCodeCommand(); + + var code = command.Execute(invalidLength); + + Assert.NotEqual(length, code.Length); + } + + + [Fact] + public void ShouldGenerateValidAlphanumericCode() + { + var length = 6; + + var command = new GenerateCodeCommand(); + + var code = command.Execute(length); + + Assert.True(code.All(x => char.IsLetterOrDigit(x))); + } + + [Fact] + public void ShouldGenerateRandomCodes() + { + var length = 6; + + var command = new GenerateCodeCommand(); + + var codes = new List + { + command.Execute(length), + command.Execute(length), + command.Execute(length), + command.Execute(length), + }; + + Assert.True(codes.Distinct().Count() == codes.Count()); + } + } +} diff --git a/src/UrlShortener/UrlShortener.Test/Commands/ShortenUrlUsageCommandTest.cs b/src/UrlShortener/UrlShortener.Test/Commands/ShortenUrlUsageCommandTest.cs new file mode 100644 index 0000000..a5a32b1 --- /dev/null +++ b/src/UrlShortener/UrlShortener.Test/Commands/ShortenUrlUsageCommandTest.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Text; +using UrlShortener.Domain.Commands; +using UrlShortener.Domain.Entities; +using Xunit; + +namespace UrlShortener.Test.Commands +{ + public class ShortenUrlUsageCommandTest + { + [Fact] + public void ShouldUpdateCountAndLastUsage() + { + var entity = new ShortUrl + { + LastUsage = null, + UsageCount = 0 + }; + + var command = new ShortenUrlUsageCommand(); + + var code = command.Execute(entity); + + Assert.Equal(1, entity.UsageCount); + Assert.NotNull(entity.LastUsage); + } + + [Fact] + public void ShouldUpdateCountAndLastUsageNow() + { + var origDate = DateTime.Now; + + var entity = new ShortUrl + { + LastUsage = origDate, + UsageCount = 1 + }; + + var command = new ShortenUrlUsageCommand(); + + var code = command.Execute(entity); + + Assert.Equal(2, entity.UsageCount); + Assert.True(entity.LastUsage > origDate); + } + } +} diff --git a/src/UrlShortener/UrlShortener.Test/Services/UrlShortenerCreatorServiceTest.cs b/src/UrlShortener/UrlShortener.Test/Services/UrlShortenerCreatorServiceTest.cs new file mode 100644 index 0000000..091103f --- /dev/null +++ b/src/UrlShortener/UrlShortener.Test/Services/UrlShortenerCreatorServiceTest.cs @@ -0,0 +1,119 @@ +using AutoMapper; +using Moq; +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; +using UrlShortener.CrossCutting.Exceptions; +using UrlShortener.Domain.Dto; +using UrlShortener.Domain.Entities; +using UrlShortener.Domain.Interfaces; +using UrlShortener.Domain.Interfaces.Validators; +using UrlShortener.Domain.Services.Creators; +using UrlShortener.Domain.Validators; +using Xunit; + +namespace UrlShortener.Test.Services +{ + public class UrlShortenerCreatorServiceTest + { + private readonly ICodeValidator codeValidator; + + public UrlShortenerCreatorServiceTest() + { + this.codeValidator = new CodeValidator(); + } + + [Fact] + public void ShouldGenerateUnprocessableEntityException() + { + var mapper = new Mock(); + mapper.Setup(x => x.Map(It.IsAny())); + + var repository = new Mock>(); + + var service = new UrlShortenerCreatorService(mapper.Object, + repository.Object, + this.codeValidator); + + var shortenUrl = new ShortUrlDto + { + Code = "AAA", + Url = "www.example.com" + }; + + Assert.Throws(() => service.Execute(shortenUrl)); + } + + [Fact] + public void ShouldGenerateConflictException() + { + var shortenUrl = new ShortUrl + { + Code = "AAA123", + Url = "www.example.com" + }; + + var mapper = new Mock(); + mapper.Setup(x => x.Map(It.IsAny())); + + var list = new List { shortenUrl }; + + var task = Task.FromResult>(list); + + var repository = new Mock>(); + + repository.Setup(x => x.ListAsync(It.IsAny>>())) + .Returns(task); + + var service = new UrlShortenerCreatorService(mapper.Object, + repository.Object, + this.codeValidator); + + var shortenUrlDto = new ShortUrlDto + { + Code = "AAA123", + Url = "www.example.com" + }; + + Assert.Throws(() => service.Execute(shortenUrlDto)); + } + + [Fact] + public void ShouldReturnShortenUrlEntityFromDto() + { + var mapper = new Mock(); + mapper.Setup(x => x.Map(It.IsAny())) + .Returns(new ShortUrl + { + Code = "AAAZZ4", + Url = "www.example.com" + }); + + var list = new List(); + + var task = Task.FromResult>(list); + + var repository = new Mock>(); + + repository.Setup(x => x.ListAsync(It.IsAny>>())) + .Returns(task); + + var service = new UrlShortenerCreatorService(mapper.Object, + repository.Object, + this.codeValidator); + + var shortenUrlDto = new ShortUrlDto + { + Code = "AAAZZ4", + Url = "www.example.com" + }; + + var result = service.Execute(shortenUrlDto); + + Assert.Equal(result.Code, shortenUrlDto.Code); + Assert.Equal(result.Url, shortenUrlDto.Url); + } + } +} diff --git a/src/UrlShortener/UrlShortener.Test/Services/UrlShortenerServiceTest.cs b/src/UrlShortener/UrlShortener.Test/Services/UrlShortenerServiceTest.cs new file mode 100644 index 0000000..1142051 --- /dev/null +++ b/src/UrlShortener/UrlShortener.Test/Services/UrlShortenerServiceTest.cs @@ -0,0 +1,229 @@ +using AutoMapper; +using Moq; +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; +using UrlShortener.CrossCutting.Exceptions; +using UrlShortener.Domain.Commands; +using UrlShortener.Domain.Dto; +using UrlShortener.Domain.Entities; +using UrlShortener.Domain.Interfaces; +using UrlShortener.Domain.Interfaces.Creators; +using UrlShortener.Domain.Services; +using Xunit; + +namespace UrlShortener.Test.Services +{ + public class UrlShortenerServiceTest + { + private readonly Mock mapper; + private readonly Mock> repository; + private readonly new Mock creatorService; + + public UrlShortenerServiceTest() + { + this.mapper = new Mock(); + this.repository = new Mock>(); + this.creatorService = new Mock(); + + this.mapper.Setup(x => x.Map(It.IsAny())); + } + + [Fact] + public void ShouldAddNewShortUrlEntity() + { + var urlShortenDto = new ShortUrlDto + { + Code = "Code1", + Url = "www.example.com" + }; + + var urlShorten = new ShortUrl + { + Code = "Code1", + Url = "www.example.com" + }; + + creatorService.Setup(x => x.Execute(urlShortenDto)) + .Returns(urlShorten); + + var service = new UrlShortenerService(repository.Object, + new GenerateCodeCommand(), + creatorService.Object, + new ShortenUrlUsageCommand(), + mapper.Object); + + + var result = service.Add(urlShortenDto); + + repository.Verify(x => x.Add(It.IsAny()), Times.Once); + Assert.Equal(urlShortenDto.Code, result.Code); + } + + [Fact] + public void ShouldAddNewShortUrlEntityFromEmptyCode() + { + var urlShortenDto = new ShortUrlDto + { + Url = "www.example.com" + }; + + var urlShorten = new ShortUrl + { + Code = "Code1", + Url = "www.example.com" + }; + + creatorService.Setup(x => x.Execute(urlShortenDto)) + .Returns(urlShorten); + + var service = new UrlShortenerService(repository.Object, + new GenerateCodeCommand(), + creatorService.Object, + new ShortenUrlUsageCommand(), + mapper.Object); + + + var result = service.Add(urlShortenDto); + + repository.Verify(x => x.Add(It.IsAny()), Times.Once); + + Assert.False(string.IsNullOrEmpty(result.Code)); + Assert.False(string.IsNullOrEmpty(urlShortenDto.Code)); + } + + [Fact] + public void ShouldReturnShortUrlEntityFromCodeAndUpdate() + { + var code = "Code1"; + + var urlShortenDto = new ShortUrlDto + { + Code = "Code1", + Url = "www.example.com" + }; + + var urlShorten = new ShortUrl + { + Code = "Code1", + Url = "www.example.com" + }; + + creatorService.Setup(x => x.Execute(urlShortenDto)) + .Returns(urlShorten); + + var list = new List + { + new ShortUrl { Code = "Code2", Url = "www.example2.com" } + }; + + var task = Task.FromResult>(list); + + repository.Setup(x => x.ListAsync(It.IsAny>>())) + .Returns(task); + + var service = new UrlShortenerService(repository.Object, + new GenerateCodeCommand(), + creatorService.Object, + new ShortenUrlUsageCommand(), + mapper.Object); + + + var result = service.GetUrlByCode(code).Result; + + repository.Verify(x => x.ListAsync(It.IsAny>>()), Times.Once); + repository.Verify(x => x.Update(It.IsAny()), Times.Once); + + Assert.False(string.IsNullOrEmpty(result)); + } + + [Fact] + public void ShouldThrowExceptionWhenNotFoundAEntityByCode() + { + var code = "Code1"; + + var urlShortenDto = new ShortUrlDto + { + Code = "Code1", + Url = "www.example.com" + }; + + var urlShorten = new ShortUrl + { + Code = "Code1", + Url = "www.example.com" + }; + + creatorService.Setup(x => x.Execute(urlShortenDto)) + .Returns(urlShorten); + + var list = new List(); + + var task = Task.FromResult>(list); + + repository.Setup(x => x.ListAsync(It.IsAny>>())) + .Returns(task); + + var service = new UrlShortenerService(repository.Object, + new GenerateCodeCommand(), + creatorService.Object, + new ShortenUrlUsageCommand(), + mapper.Object); + + Assert.ThrowsAsync(() => service.GetUrlByCode(code)); + } + + [Fact] + public void ShoulReturnStatsByCode() + { + var code = "Code1"; + + var statDto = new UrlStatDto + { + CreatedAt = DateTime.Now.AddDays(-2), + LastUsage = DateTime.Now, + UsageCount = 1 + }; + + var urlShortenDto = new ShortUrlDto + { + Code = "Code1", + Url = "www.example.com" + }; + + var urlShorten = new ShortUrl + { + Code = "Code1", + Url = "www.example.com" + }; + + this.mapper.Setup(x => x.Map(urlShorten)) + .Returns(statDto); + + creatorService.Setup(x => x.Execute(urlShortenDto)) + .Returns(urlShorten); + + var list = new List { urlShorten }; + + var task = Task.FromResult>(list); + + repository.Setup(x => x.ListAsync(It.IsAny>>())) + .Returns(task); + + var service = new UrlShortenerService(repository.Object, + new GenerateCodeCommand(), + creatorService.Object, + new ShortenUrlUsageCommand(), + mapper.Object); + + var result = Task.Run(() => service.GetStatByCode(code)).Result; + + repository.Verify(x => x.ListAsync(It.IsAny>>()), Times.Once); + Assert.Equal(1, result.UsageCount); + Assert.True(DateTime.Now > result.CreatedAt); + Assert.True(result.UsageCount > 0); + } + } +} diff --git a/src/UrlShortener/UrlShortener.Test/UrlShortener.Test.csproj b/src/UrlShortener/UrlShortener.Test/UrlShortener.Test.csproj new file mode 100644 index 0000000..4e89474 --- /dev/null +++ b/src/UrlShortener/UrlShortener.Test/UrlShortener.Test.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp3.0 + + false + + + + + + + + + + + + + + + diff --git a/src/UrlShortener/UrlShortener.Test/Validators/CodeValidatorTest.cs b/src/UrlShortener/UrlShortener.Test/Validators/CodeValidatorTest.cs new file mode 100644 index 0000000..015640b --- /dev/null +++ b/src/UrlShortener/UrlShortener.Test/Validators/CodeValidatorTest.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Text; +using UrlShortener.Domain.Commands; +using UrlShortener.Domain.Validators; +using Xunit; + +namespace UrlShortener.Test.Validators +{ + public class CodeValidatorTest + { + [Fact] + public void ShouldGenerateValidLengthCode() + { + var length = 6; + var command = new GenerateCodeCommand(); + + var code = command.Execute(length); + + var validator = new CodeValidator(); + + Assert.True(validator.Execute(code)); + } + + [Fact] + public void ShouldGenerateInvalidLengthCode() + { + var length = 8; + var command = new GenerateCodeCommand(); + + var code = command.Execute(length); + + var validator = new CodeValidator(); + + Assert.False(validator.Execute(code)); + } + + [Fact] + public void ShouldBeValidCode() + { + var code = "aaa123"; + + var validator = new CodeValidator(); + + Assert.True(validator.Execute(code)); + } + + [Fact] + public void ShouldBeInvalidCode() + { + var code = "aaabbb1"; + + var validator = new CodeValidator(); + + Assert.False(validator.Execute(code)); + } + } +} diff --git a/src/UrlShortener/UrlShortener.sln b/src/UrlShortener/UrlShortener.sln new file mode 100644 index 0000000..9a424df --- /dev/null +++ b/src/UrlShortener/UrlShortener.sln @@ -0,0 +1,49 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29424.173 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UrlShortener.Api", "UrlShortener.Api\UrlShortener.Api.csproj", "{AABECE49-1623-4754-AF64-2EF23DC2391A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UrlShortener.Domain", "Urlshortener.Domain\UrlShortener.Domain.csproj", "{31C4F187-6788-4859-8617-06D8B1B8A2EE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UrlShortener.Infra", "UrlShortener.Infra\UrlShortener.Infra.csproj", "{7EE9A792-BFEE-421C-BDBA-E7EA666F24D0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UrlShortener.CrossCutting", "CrossCutting\UrlShortener.CrossCutting.csproj", "{96971120-6403-47F7-8A8C-49B696812B68}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UrlShortener.Test", "UrlShortener.Test\UrlShortener.Test.csproj", "{7D03AC66-4C55-44C5-B075-60E4D92FB9A2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AABECE49-1623-4754-AF64-2EF23DC2391A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AABECE49-1623-4754-AF64-2EF23DC2391A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AABECE49-1623-4754-AF64-2EF23DC2391A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AABECE49-1623-4754-AF64-2EF23DC2391A}.Release|Any CPU.Build.0 = Release|Any CPU + {31C4F187-6788-4859-8617-06D8B1B8A2EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {31C4F187-6788-4859-8617-06D8B1B8A2EE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {31C4F187-6788-4859-8617-06D8B1B8A2EE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {31C4F187-6788-4859-8617-06D8B1B8A2EE}.Release|Any CPU.Build.0 = Release|Any CPU + {7EE9A792-BFEE-421C-BDBA-E7EA666F24D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7EE9A792-BFEE-421C-BDBA-E7EA666F24D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7EE9A792-BFEE-421C-BDBA-E7EA666F24D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7EE9A792-BFEE-421C-BDBA-E7EA666F24D0}.Release|Any CPU.Build.0 = Release|Any CPU + {96971120-6403-47F7-8A8C-49B696812B68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {96971120-6403-47F7-8A8C-49B696812B68}.Debug|Any CPU.Build.0 = Debug|Any CPU + {96971120-6403-47F7-8A8C-49B696812B68}.Release|Any CPU.ActiveCfg = Release|Any CPU + {96971120-6403-47F7-8A8C-49B696812B68}.Release|Any CPU.Build.0 = Release|Any CPU + {7D03AC66-4C55-44C5-B075-60E4D92FB9A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7D03AC66-4C55-44C5-B075-60E4D92FB9A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7D03AC66-4C55-44C5-B075-60E4D92FB9A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7D03AC66-4C55-44C5-B075-60E4D92FB9A2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D536D402-7E1B-4604-ADF2-6FAC27348BEE} + EndGlobalSection +EndGlobal diff --git a/src/UrlShortener/Urlshortener.Domain/Commands/GenerateCodeCommand.cs b/src/UrlShortener/Urlshortener.Domain/Commands/GenerateCodeCommand.cs new file mode 100644 index 0000000..e679e1e --- /dev/null +++ b/src/UrlShortener/Urlshortener.Domain/Commands/GenerateCodeCommand.cs @@ -0,0 +1,13 @@ +using UrlShortener.CrossCutting; +using UrlShortener.Domain.Interfaces.Commands; + +namespace UrlShortener.Domain.Commands +{ + public class GenerateCodeCommand : IGenerateCodeCommand + { + public string Execute(int lenght) + { + return CodeUtilities.Generate(lenght); + } + } +} diff --git a/src/UrlShortener/Urlshortener.Domain/Commands/ShortenUrlUsageCommand.cs b/src/UrlShortener/Urlshortener.Domain/Commands/ShortenUrlUsageCommand.cs new file mode 100644 index 0000000..85c4562 --- /dev/null +++ b/src/UrlShortener/Urlshortener.Domain/Commands/ShortenUrlUsageCommand.cs @@ -0,0 +1,17 @@ +using System; +using UrlShortener.Domain.Entities; +using UrlShortener.Domain.Interfaces.Commands; + +namespace UrlShortener.Domain.Commands +{ + public class ShortenUrlUsageCommand : IShortenUrlUsageCommand + { + public ShortUrl Execute(ShortUrl entity) + { + entity.LastUsage = DateTime.Now; + entity.UsageCount = ++entity.UsageCount; + + return entity; + } + } +} diff --git a/src/UrlShortener/Urlshortener.Domain/Dto/ShortUrCreatedlDto.cs b/src/UrlShortener/Urlshortener.Domain/Dto/ShortUrCreatedlDto.cs new file mode 100644 index 0000000..c136b52 --- /dev/null +++ b/src/UrlShortener/Urlshortener.Domain/Dto/ShortUrCreatedlDto.cs @@ -0,0 +1,8 @@ + +namespace UrlShortener.Domain.Dto +{ + public class ShortUrCreatedlDto + { + public string Code { get; set; } + } +} diff --git a/src/UrlShortener/Urlshortener.Domain/Dto/ShortUrlDto.cs b/src/UrlShortener/Urlshortener.Domain/Dto/ShortUrlDto.cs new file mode 100644 index 0000000..4f52376 --- /dev/null +++ b/src/UrlShortener/Urlshortener.Domain/Dto/ShortUrlDto.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace UrlShortener.Domain.Dto +{ + public class ShortUrlDto + { + public string Code { get; set; } + + [Required] + public string Url { get; set; } + } +} diff --git a/src/UrlShortener/Urlshortener.Domain/Dto/UrlStatDto.cs b/src/UrlShortener/Urlshortener.Domain/Dto/UrlStatDto.cs new file mode 100644 index 0000000..f1b8116 --- /dev/null +++ b/src/UrlShortener/Urlshortener.Domain/Dto/UrlStatDto.cs @@ -0,0 +1,34 @@ +using System; +using System.Text.Json.Serialization; + +namespace UrlShortener.Domain.Dto +{ + public class UrlStatDto + { + [JsonIgnore] + public DateTime CreatedAt { get; set; } + + [JsonIgnore] + public DateTime? LastUsage { get; set; } + + public int UsageCount { get; set; } + + [JsonPropertyName("CreatedAt")] + public string CreatedAtUTC + { + get + { + return CreatedAt.ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ssZ"); + } + } + + [JsonPropertyName("LastUsage")] + public string LastUsageUTC + { + get + { + return LastUsage != null ? LastUsage.Value.ToUniversalTime().ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ssZ") : null; + } + } + } +} diff --git a/src/UrlShortener/Urlshortener.Domain/Entities/BaseEntity.cs b/src/UrlShortener/Urlshortener.Domain/Entities/BaseEntity.cs new file mode 100644 index 0000000..960ba05 --- /dev/null +++ b/src/UrlShortener/Urlshortener.Domain/Entities/BaseEntity.cs @@ -0,0 +1,9 @@ +using System; + +namespace UrlShortener.Domain.Entities +{ + public class BaseEntity + { + public Guid Id { get; set; } + } +} diff --git a/src/UrlShortener/Urlshortener.Domain/Entities/ShortUrl.cs b/src/UrlShortener/Urlshortener.Domain/Entities/ShortUrl.cs new file mode 100644 index 0000000..cdd340f --- /dev/null +++ b/src/UrlShortener/Urlshortener.Domain/Entities/ShortUrl.cs @@ -0,0 +1,17 @@ +using System; + +namespace UrlShortener.Domain.Entities +{ + public class ShortUrl : BaseEntity + { + public string Code { get; set; } + + public string Url { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime? LastUsage { get; set; } + + public int UsageCount { get; set; } + } +} diff --git a/src/UrlShortener/Urlshortener.Domain/Interfaces/Commands/IGenerateCodeCommand.cs b/src/UrlShortener/Urlshortener.Domain/Interfaces/Commands/IGenerateCodeCommand.cs new file mode 100644 index 0000000..61e9404 --- /dev/null +++ b/src/UrlShortener/Urlshortener.Domain/Interfaces/Commands/IGenerateCodeCommand.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace UrlShortener.Domain.Interfaces.Commands +{ + public interface IGenerateCodeCommand : ICommandWithResult + { + } +} diff --git a/src/UrlShortener/Urlshortener.Domain/Interfaces/Commands/IShortenUrlUsageCommand.cs b/src/UrlShortener/Urlshortener.Domain/Interfaces/Commands/IShortenUrlUsageCommand.cs new file mode 100644 index 0000000..53c9a71 --- /dev/null +++ b/src/UrlShortener/Urlshortener.Domain/Interfaces/Commands/IShortenUrlUsageCommand.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; +using UrlShortener.Domain.Entities; + +namespace UrlShortener.Domain.Interfaces.Commands +{ + public interface IShortenUrlUsageCommand : ICommandWithResult + { + } +} diff --git a/src/UrlShortener/Urlshortener.Domain/Interfaces/Creators/IUrlShortenerCreatorService.cs b/src/UrlShortener/Urlshortener.Domain/Interfaces/Creators/IUrlShortenerCreatorService.cs new file mode 100644 index 0000000..048278e --- /dev/null +++ b/src/UrlShortener/Urlshortener.Domain/Interfaces/Creators/IUrlShortenerCreatorService.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; +using UrlShortener.Domain.Dto; +using UrlShortener.Domain.Entities; + +namespace UrlShortener.Domain.Interfaces.Creators +{ + public interface IUrlShortenerCreatorService + { + ShortUrl Execute(ShortUrlDto shortUrlDto); + } +} diff --git a/src/UrlShortener/Urlshortener.Domain/Interfaces/ICommandWithResult.cs b/src/UrlShortener/Urlshortener.Domain/Interfaces/ICommandWithResult.cs new file mode 100644 index 0000000..384de13 --- /dev/null +++ b/src/UrlShortener/Urlshortener.Domain/Interfaces/ICommandWithResult.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace UrlShortener.Domain.Interfaces +{ + public interface ICommandWithResult + { + TResult Execute(TEntity param); + } +} diff --git a/src/UrlShortener/Urlshortener.Domain/Interfaces/ICommandWithoutResult.cs b/src/UrlShortener/Urlshortener.Domain/Interfaces/ICommandWithoutResult.cs new file mode 100644 index 0000000..c0d7960 --- /dev/null +++ b/src/UrlShortener/Urlshortener.Domain/Interfaces/ICommandWithoutResult.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace UrlShortener.Domain.Interfaces +{ + public interface ICommandWithoutResult + { + void Execute(TEntity param); + } +} diff --git a/src/UrlShortener/Urlshortener.Domain/Interfaces/IRepository.cs b/src/UrlShortener/Urlshortener.Domain/Interfaces/IRepository.cs new file mode 100644 index 0000000..ba5373f --- /dev/null +++ b/src/UrlShortener/Urlshortener.Domain/Interfaces/IRepository.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; +using UrlShortener.Domain.Entities; + +namespace UrlShortener.Domain.Interfaces +{ + public interface IRepository where T : BaseEntity + { + Task GetByIdAsync(string id); + + Task> ListAsync(Expression> filter = null); + + void Add(T entity); + void Update(T entity); + void Delete(T entity); + } +} diff --git a/src/UrlShortener/Urlshortener.Domain/Interfaces/IUrlShortenerService.cs b/src/UrlShortener/Urlshortener.Domain/Interfaces/IUrlShortenerService.cs new file mode 100644 index 0000000..6c64b3d --- /dev/null +++ b/src/UrlShortener/Urlshortener.Domain/Interfaces/IUrlShortenerService.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using UrlShortener.Domain.Dto; +using UrlShortener.Domain.Entities; + +namespace UrlShortener.Domain.Interfaces +{ + public interface IUrlShortenerService + { + ShortUrCreatedlDto Add(ShortUrlDto shortUrl); + + Task GetUrlByCode(string code); + + Task GetStatByCode(string code); + } +} diff --git a/src/UrlShortener/Urlshortener.Domain/Interfaces/Validators/ICodeValidator.cs b/src/UrlShortener/Urlshortener.Domain/Interfaces/Validators/ICodeValidator.cs new file mode 100644 index 0000000..9f390d2 --- /dev/null +++ b/src/UrlShortener/Urlshortener.Domain/Interfaces/Validators/ICodeValidator.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace UrlShortener.Domain.Interfaces.Validators +{ + public interface ICodeValidator : ICommandWithResult + { + } +} diff --git a/src/UrlShortener/Urlshortener.Domain/Services/Creators/UrlShortenerBaseCreatorService.cs b/src/UrlShortener/Urlshortener.Domain/Services/Creators/UrlShortenerBaseCreatorService.cs new file mode 100644 index 0000000..9f0fba1 --- /dev/null +++ b/src/UrlShortener/Urlshortener.Domain/Services/Creators/UrlShortenerBaseCreatorService.cs @@ -0,0 +1,37 @@ +using AutoMapper; +using UrlShortener.Domain.Dto; +using UrlShortener.Domain.Entities; +using UrlShortener.Domain.Interfaces.Creators; + +namespace UrlShortener.Domain.Services.UrlShortenerCreatorService +{ + public abstract class UrlShortenerBaseCreatorService : IUrlShortenerCreatorService + { + private readonly IMapper mapper; + + public UrlShortenerBaseCreatorService(IMapper mapper) + { + this.mapper = mapper; + } + + protected abstract bool IsValid(string code); + protected abstract void UnprocessableEntity(); + protected abstract bool IsConflictEntity(string code); + protected abstract void ConflictEntity(string code); + + public ShortUrl Execute(ShortUrlDto shortUrlDto) + { + if (!IsValid(shortUrlDto.Code)) + { + UnprocessableEntity(); + } + + if (IsConflictEntity(shortUrlDto.Code)) + { + ConflictEntity(shortUrlDto.Code); + } + + return this.mapper.Map(shortUrlDto); + } + } +} diff --git a/src/UrlShortener/Urlshortener.Domain/Services/Creators/UrlShortenerCreatorService.cs b/src/UrlShortener/Urlshortener.Domain/Services/Creators/UrlShortenerCreatorService.cs new file mode 100644 index 0000000..117880a --- /dev/null +++ b/src/UrlShortener/Urlshortener.Domain/Services/Creators/UrlShortenerCreatorService.cs @@ -0,0 +1,49 @@ +using AutoMapper; +using System.Linq; +using System.Threading.Tasks; +using UrlShortener.CrossCutting.Exceptions; +using UrlShortener.Domain.Entities; +using UrlShortener.Domain.Interfaces; +using UrlShortener.Domain.Interfaces.Validators; +using UrlShortener.Domain.Services.UrlShortenerCreatorService; + +namespace UrlShortener.Domain.Services.Creators +{ + public class UrlShortenerCreatorService : UrlShortenerBaseCreatorService + { + private readonly IRepository repository; + private readonly ICodeValidator codeValidator; + + public UrlShortenerCreatorService(IMapper mapper, + IRepository repository, + ICodeValidator codeValidator) : base(mapper) + { + this.repository = repository; + this.codeValidator = codeValidator; + } + + protected override void ConflictEntity(string code) + { + throw new ConflictException($"Code [{code}] is already in use"); + } + + protected override bool IsConflictEntity(string code) + { + var task = Task.Run(async () => await this.repository.ListAsync(x => x.Code.Equals(code))); + + var result = task.Result; + + return result.FirstOrDefault() != null; + } + + protected override bool IsValid(string code) + { + return this.codeValidator.Execute(code); + } + + protected override void UnprocessableEntity() + { + throw new UnprocessableEntityException($"Code must be Alphanumeric and 6 chars lenght"); + } + } +} diff --git a/src/UrlShortener/Urlshortener.Domain/Services/UrlShortenerService.cs b/src/UrlShortener/Urlshortener.Domain/Services/UrlShortenerService.cs new file mode 100644 index 0000000..50b7ed4 --- /dev/null +++ b/src/UrlShortener/Urlshortener.Domain/Services/UrlShortenerService.cs @@ -0,0 +1,94 @@ +using AutoMapper; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using UrlShortener.CrossCutting.Exceptions; +using UrlShortener.Domain.Dto; +using UrlShortener.Domain.Entities; +using UrlShortener.Domain.Interfaces; +using UrlShortener.Domain.Interfaces.Commands; +using UrlShortener.Domain.Interfaces.Creators; + +namespace UrlShortener.Domain.Services +{ + public class UrlShortenerService : IUrlShortenerService + { + private readonly IRepository repository; + private readonly IGenerateCodeCommand generateCodeCommand; + private readonly IMapper mapper; + private readonly IUrlShortenerCreatorService urlShortenerCreatorService; + private readonly IShortenUrlUsageCommand shortenUrlUsageCommand; + + private static int length = 6; + + public UrlShortenerService(IRepository repository, + IGenerateCodeCommand generateCodeCommand, + IUrlShortenerCreatorService urlShortenerCreatorService, + IShortenUrlUsageCommand shortenUrlUsageCommand, + IMapper mapper) + { + this.repository = repository; + this.generateCodeCommand = generateCodeCommand; + this.mapper = mapper; + this.urlShortenerCreatorService = urlShortenerCreatorService; + this.shortenUrlUsageCommand = shortenUrlUsageCommand; + } + + public ShortUrCreatedlDto Add(ShortUrlDto shortUrlDto) + { + var shortUrl = CreateShortenUrl(shortUrlDto); + + this.repository.Add(shortUrl); + + return new ShortUrCreatedlDto { Code = shortUrl.Code} ; + } + + public async Task GetUrlByCode(string code) + { + var query = await this.repository.ListAsync(x => x.Code.Equals(code)); + + var entity = GetEntityByCode(query, code); + + entity = this.shortenUrlUsageCommand.Execute(entity); + + this.repository.Update(entity); + + return entity.Url; + } + + public async Task GetStatByCode(string code) + { + var query = await this.repository.ListAsync(x => x.Code.Equals(code)); + + var entity = GetEntityByCode(query, code); + + return this.mapper.Map(entity); + } + + private ShortUrl GetEntityByCode(IList query, string code) + { + var entity = query.FirstOrDefault(); + + if (entity == null) + { + throw new NotFoundException($"The Code [{code}] cannot be found"); + } + + return entity; + } + + private ShortUrl CreateShortenUrl(ShortUrlDto shortUrlDto) + { + if (string.IsNullOrEmpty(shortUrlDto.Code)) + { + shortUrlDto.Code = GenerateCode(); + } + + var shortUrlEntity = urlShortenerCreatorService.Execute(shortUrlDto); + + return shortUrlEntity; + } + + private string GenerateCode() => this.generateCodeCommand.Execute(length); + } +} diff --git a/src/UrlShortener/Urlshortener.Domain/UrlShortener.Domain.csproj b/src/UrlShortener/Urlshortener.Domain/UrlShortener.Domain.csproj new file mode 100644 index 0000000..0096e1c --- /dev/null +++ b/src/UrlShortener/Urlshortener.Domain/UrlShortener.Domain.csproj @@ -0,0 +1,15 @@ + + + + netcoreapp3.0 + + + + + + + + + + + diff --git a/src/UrlShortener/Urlshortener.Domain/Validators/CodeValidator.cs b/src/UrlShortener/Urlshortener.Domain/Validators/CodeValidator.cs new file mode 100644 index 0000000..ebe6693 --- /dev/null +++ b/src/UrlShortener/Urlshortener.Domain/Validators/CodeValidator.cs @@ -0,0 +1,13 @@ +using System.Linq; +using UrlShortener.Domain.Interfaces.Validators; + +namespace UrlShortener.Domain.Validators +{ + public class CodeValidator : ICodeValidator + { + public bool Execute(string code) + { + return code.All(x => char.IsLetterOrDigit(x)) && code.Length == 6; + } + } +}