Skip to content

Commit

Permalink
initial setup
Browse files Browse the repository at this point in the history
  • Loading branch information
ozkank committed Jul 2, 2024
1 parent 340801a commit 1ec4402
Show file tree
Hide file tree
Showing 15 changed files with 315 additions and 116 deletions.
25 changes: 25 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
135 changes: 79 additions & 56 deletions UrlShortener.ApiService/Features/RedirectUrl.cs
Original file line number Diff line number Diff line change
@@ -1,56 +1,79 @@
using Carter;
using MediatR;
using Microsoft.EntityFrameworkCore;
using UrlShortener.ApiService.Infrastructure.Database;

namespace UrlShortener.ApiService.Features
{
public class RedirectUrlQuery : IRequest<string>
{
public string Code { get; set; }
}

internal sealed class RedirectUrlQueryHandler :
IRequestHandler<RedirectUrlQuery, string>
{
private readonly ApplicationDbContext _context;
public const int NumberOfCharsInShortLink = 7;
private const string Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

private readonly Random _random = new();

public RedirectUrlQueryHandler(ApplicationDbContext context)
{
_context = context;
}

public async Task<string> Handle(RedirectUrlQuery request, CancellationToken cancellationToken)
{
var shortenedUrl = await _context
.ShortenedUrls
.FirstOrDefaultAsync(s => s.Code == request.Code);

if (shortenedUrl is null)
{
//return Results.NotFound();
}

return shortenedUrl.ShortUrl;
}
}


public class RedirectUrlEndpoint : ICarterModule
{
public void AddRoutes(IEndpointRouteBuilder app)
{
app.MapGet("api/{code}", async (String code, ISender sender) =>
{
var query = new RedirectUrlQuery { Code = code };
var result = await sender.Send(query);

return Results.Ok(result);
});
}
}
}
//using Azure.Core;
//using Carter;
//using FluentValidation;
//using MediatR;
//using Microsoft.EntityFrameworkCore;
//using UrlShortener.ApiService.Infrastructure.Database;
//using UrlShortener.ApiService.Shared;

//namespace UrlShortener.ApiService.Features
//{
// public class RedirectUrl
// {
// public class Query : IRequest<Result<RedirectUrlResponse>>
// {
// public string Code { get; set; } = string.Empty;
// }

// internal sealed class Handler : IRequestHandler<Query, Result<RedirectUrlResponse>>
// {
// private readonly ApplicationDbContext _context;
// public const int NumberOfCharsInShortLink = 7;
// private const string Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

// private readonly Random _random = new();

// public Handler(ApplicationDbContext context)
// {
// _context = context;
// }

// public async Task<Result<RedirectUrlResponse>> Handle(Query request, CancellationToken cancellationToken)
// {
// var item = await _context
// .ShortenedUrls
// .Where(s => s.Code == request.Code)
// .Select(x => new RedirectUrlResponse
// {
// LongUrl = x.LongUrl,
// })
// .FirstOrDefaultAsync(cancellationToken);

// if (item is null)
// {
// return Result.Failure<RedirectUrlResponse>(new Error(
// "RedirectUrlResponse.Null",
// "The longUrl with the specified shortUrl was not found"));
// }

// return item;
// }
// }

// }

// public class RedirectUrlEndpoint : ICarterModule
// {
// public void AddRoutes(IEndpointRouteBuilder app)
// {
// app.MapGet("api/redirect/{shortUrl}", async (string shortUrl, ISender sender) =>
// {
// var query = new RedirectUrl.Query { Code = shortUrl };

// var result = await sender.Send(query);

// if (result.IsFailure)
// {
// return Results.NotFound(result.Error);
// }

// return Results.Ok(result.Value);
// });
// }
// }

// public class RedirectUrlResponse
// {
// public string LongUrl { get; set; } = string.Empty;
// }
//}
114 changes: 66 additions & 48 deletions UrlShortener.ApiService/Features/ShortenUrl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,73 +6,86 @@
using System;
using UrlShortener.ApiService.Domain;
using UrlShortener.ApiService.Infrastructure.Database;
using UrlShortener.ApiService.Shared;

namespace UrlShortener.ApiService.Features
{
public class ShortenUrlQuery : IRequest<string>
public class ShortenUrl
{
public string LongUrl { get; set; }
}


internal sealed class ShortenUrlQueryHandler :
IRequestHandler<ShortenUrlQuery, string>
{
private readonly ApplicationDbContext _context;
public const int NumberOfCharsInShortLink = 7;
private const string Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

private readonly Random _random = new();

public ShortenUrlQueryHandler(ApplicationDbContext context)
public class Command : IRequest<Result<string>>
{
_context = context;
public string LongUrl { get; set; }
}

public async Task<string> Handle(ShortenUrlQuery request, CancellationToken cancellationToken)
public class Validator : AbstractValidator<Command>
{
if (!Uri.TryCreate(request.LongUrl, UriKind.Absolute, out _))
public Validator()
{
throw new Exception("The specified URL is invalid.");
//return Results.BadRequest("The specified URL is invalid.");
RuleFor(c => c.LongUrl).NotEmpty();
}
}

var code = await GenerateUniqueCode();
internal sealed class Handler : IRequestHandler<Command, Result<string>>
{
private readonly ApplicationDbContext _dbContext;
private readonly IValidator<Command> _validator;
public const int NumberOfCharsInShortLink = 7;
private const string Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
private readonly Random _random = new();

var shortenedUrl = new ShortenedUrl
public Handler(ApplicationDbContext dbContext, IValidator<Command> validator)
{
Id = Guid.NewGuid(),
LongUrl = request.LongUrl,
Code = code,
//ShortUrl = $"{_context.Request.Scheme}://{_context.Request.Host}/api/{code}",
CreatedOnUtc = DateTime.UtcNow
};
_dbContext = dbContext;
_validator = validator;
}

_context.ShortenedUrls.Add(shortenedUrl);
public async Task<Result<string>> Handle(Command request, CancellationToken cancellationToken)
{
var validationResult = _validator.Validate(request);
if (!validationResult.IsValid)
{
return Result.Failure<string>(new Error(
"ShortenUrl.Validation",
validationResult.ToString()));
}

await _context.SaveChangesAsync();
var code = await GenerateUniqueCode();

return shortenedUrl.ShortUrl;
}
var shortenedUrl = new ShortenedUrl
{
Id = Guid.NewGuid(),
LongUrl = request.LongUrl,
Code = code,
//ShortUrl = $"{httpContext.Request.Scheme}://{httpContext.Request.Host}/api/{code}",
CreatedOnUtc = DateTime.UtcNow
};

public async Task<string> GenerateUniqueCode()
{
var codeChars = new char[NumberOfCharsInShortLink];
_dbContext.ShortenedUrls.Add(shortenedUrl);

await _dbContext.SaveChangesAsync();

while (true)
return shortenedUrl.ShortUrl;
}

private async Task<string> GenerateUniqueCode()
{
for (var i = 0; i < NumberOfCharsInShortLink; i++)
var codeChars = new char[NumberOfCharsInShortLink];

while (true)
{
var randomIndex = _random.Next(Alphabet.Length - 1);
for (var i = 0; i < NumberOfCharsInShortLink; i++)
{
var randomIndex = _random.Next(Alphabet.Length - 1);

codeChars[i] = Alphabet[randomIndex];
}
codeChars[i] = Alphabet[randomIndex];
}

var code = new string(codeChars);
var code = new string(codeChars);

if (!await _context.ShortenedUrls.AnyAsync(s => s.Code == code))
{
return code;
if (!await _dbContext.ShortenedUrls.AnyAsync(s => s.Code == code))
{
return code;
}
}
}
}
Expand All @@ -82,13 +95,18 @@ public class ShortenUrlEndpoint : ICarterModule
{
public void AddRoutes(IEndpointRouteBuilder app)
{
app.MapGet("api/shorten/{longUrl}", async (String longUrl, ISender sender) =>
app.MapPost("api/articles", async (ShortenUrl.Command request, ISender sender) =>
{
var query = new ShortenUrlQuery { LongUrl = longUrl };
var result = await sender.Send(query);
var result = await sender.Send(request);

if (result.IsFailure)
{
return Results.BadRequest(result.Error);
}

return Results.Ok(result);
return Results.Ok(result.Value);
});
}
}

}
8 changes: 5 additions & 3 deletions UrlShortener.ApiService/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

var builder = WebApplication.CreateBuilder(args);

builder.AddSqlServerDbContext<ApplicationDbContext>("sqldata");

// Add service defaults & Aspire components.
builder.AddServiceDefaults();

Expand All @@ -15,13 +17,13 @@
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddDbContext<ApplicationDbContext>(o =>
o.UseSqlServer(builder.Configuration.GetConnectionString("Database")));
//builder.Services.AddDbContext<ApplicationDbContext>(o =>
// o.UseSqlServer(builder.Configuration.GetConnectionString("Database")));

builder.Services.AddEndpoints(typeof(Program).Assembly);


//builder.Services.AddMediatR(config => config.RegisterServicesFromAssembly(typeof(Program).Assembly));
builder.Services.AddMediatR(config => config.RegisterServicesFromAssembly(typeof(Program).Assembly));

builder.Services.AddCarter();

Expand Down
11 changes: 11 additions & 0 deletions UrlShortener.ApiService/Shared/Error.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace UrlShortener.ApiService.Shared
{
public record Error(string Code, string Message)
{
public static readonly Error None = new(string.Empty, string.Empty);

public static readonly Error NullValue = new("Error.NullValue", "The specified result value is null.");

public static readonly Error ConditionNotMet = new("Error.ConditionNotMet", "The specified condition was not met.");
}
}
39 changes: 39 additions & 0 deletions UrlShortener.ApiService/Shared/Result.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace UrlShortener.ApiService.Shared
{
public class Result
{
protected internal Result(bool isSuccess, Error error)
{
if (isSuccess && error != Error.None)
{
throw new InvalidOperationException();
}

if (!isSuccess && error == Error.None)
{
throw new InvalidOperationException();
}

IsSuccess = isSuccess;
Error = error;
}

public bool IsSuccess { get; }

public bool IsFailure => !IsSuccess;

public Error Error { get; }

public static Result Success() => new(true, Error.None);

public static Result<TValue> Success<TValue>(TValue value) => new(value, true, Error.None);

public static Result Failure(Error error) => new(false, error);

public static Result<TValue> Failure<TValue>(Error error) => new(default, false, error);

public static Result Create(bool condition) => condition ? Success() : Failure(Error.ConditionNotMet);

public static Result<TValue> Create<TValue>(TValue? value) => value is not null ? Success(value) : Failure<TValue>(Error.NullValue);
}
}
Loading

0 comments on commit 1ec4402

Please sign in to comment.