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;
+ }
+ }
+}