A straightforward yet effective tool for shortening long URLs into more manageable chunks is a URL shortener. This is especially helpful for reducing clutter and sharing links on platforms that have character limits. Bitly and TinyURL are two widely used URL shorteners. Creating a URL shortener presents an engaging challenge with enjoyable puzzles to solve.
However, how would one construct a URL shortener using.NET?
URL shorteners are primarily used for two purposes:
- Creating a special code for a specified URL
- Directing visitors to the original URL from the short link
I’ll walk you through the planning, execution, and factors to take into account when making your URL shortener today.
URL Shortener System Design
The high-level system architecture for our URL shortener is shown here. Two endpoints are what we wish to expose. One is used to shorten long URLs, and the other is used to reroute users to shorter URLs. In this example, the shortened URLs are kept in a PostgreSQL database. To enhance read performance, we can add a distributed cache to the system, similar to Redis.
First, a large number of short URLs must be guaranteed. Every long URL will have a unique code that will be used to create the shortened URL. The number of short URLs the system can generate depends on the unique code length and character set. When we put unique code generation into practice, we will talk about this in greater detail.
The random code generation approach will be employed. It has a respectably low collision rate and is easy to implement. We are willing to accept higher latency as a trade-off, but we will also consider other possibilities.
The Data Model
First, let’s decide what information goes into the database. Our data model is simple to understand. The URLs kept in our system are represented by the ShortenedUrl
class:
public class ShortenedUrl
{
public Guid Id { get; set; }
public string LongUrl { get; set; } = string.Empty;
public string ShortUrl { get; set; } = string.Empty;
public string Code { get; set; } = string.Empty;
public DateTime CreatedOnUtc { get; set; }
}
The shortened URL (ShortUrl
), the original URL (LongUrl
), and a special code (Code
) that denotes the shortened URL are all represented by properties in this class. Database and tracking purposes make use of the Id
and CreatedOnUtc
fields. Our system will receive the unique Code from the users and attempt to find a matching LongUrl
in order to redirect them.
We will also define an EF ApplicationDbContext
class, which will take care of establishing our database context and defining our entity. Here, I’m attempting two things to boost output:
- Using
HasMaxLength
to configure the maximum length of thecode
- Establishing a distinct index in the
Code
column
We can avoid concurrency conflicts by using a unique index, which ensures that duplicate Code
values are never stored in the database. In certain databases, indexing string columns requires setting the maximum length for this column, which also conserves storage space.
Be aware that some databases handle strings without regard to case. This drastically lowers the quantity of short URLs that are available. It is your goal to set up the database so that it handles the unique code with case sensitivity.
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions options)
: base(options)
{
}
public DbSet<ShortenedUrl> ShortenedUrls { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ShortenedUrl>(builder =>
{
builder
.Property(shortenedUrl => shortenedUrl.Code)
.HasMaxLength(ShortLinkSettings.Length);
builder
.HasIndex(shortenedUrl => shortenedUrl.Code)
.IsUnique();
});
}
}
Unique Code Generation
Making a unique code for every URL is the most important function of our URL shortener. You can apply this using a variety of different algorithms. A uniform distribution of distinct codes across all feasible values is what we aim for. This lessens the chance of collisions.
I’m going to put in place a unique, random code generator with a preset alphabet. It is easy to put into practice, and there is little chance of a collision. There are, however, still more effective options available; however, more on this later.
Let’s define a class called ShortLinkSettings
, which has two constants in it. One is to specify how long the unqualified code we produce will be. The alphabet that will be used to create the random code is the other constant.
public static class ShortLinkSettings
{
public const int Length = 7;
public const string Alphabet =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
}
With 62
characters in the alphabet, there are 62^7
unique code combinations that could be created.
This is 3,521,614,606,208
combinations, in case you were wondering.
Three trillion, five hundred twenty-one billion, six hundred fourteen million, six hundred six thousand, and two hundred eight are written out.
That is a good number of unique codes, more than enough for our URL shortener.
Let’s now put our UrlShorteningService
—which creates unique codes—into practice. Using our predefined alphabet, this service creates a random string of the given length. To verify uniqueness, a database check is performed.
public class UrlShorteningService(ApplicationDbContext dbContext)
{
private readonly Random _random = new();
public async Task<string> GenerateUniqueCode()
{
var codeChars = new char[ShortLinkSettings.Length];
const int maxValue = ShortLinkSettings.Alphabet.Length;
while (true)
{
for (var i = 0; i < ShortLinkSettings.Length; i++)
{
var randomIndex = _random.Next(maxValue);
codeChars[i] = ShortLinkSettings.Alphabet[randomIndex];
}
var code = new string(codeChars);
if (!await dbContext.ShortenedUrls.AnyAsync(s => s.Code == code))
{
return code;
}
}
}
}
Downsides and Improvement Points
The implementation’s drawback is higher latency as a result of our database-based check for every code we generate. Preparing the unique codes in the database in advance could be a point of improvement.
An infinite loop could be replaced with a fixed number of iterations as another area for improvement. If there are several consecutive collisions, the current implementation will keep going until a unique value is discovered. If there are several consecutive collisions, think about throwing an exception instead.
URL Shortening
Our main business logic is now complete, and we can expose an endpoint for URL shortening. A straightforward Minimal API endpoint will do.
After accepting a URL and validating it, this endpoint creates a shortened URL using the UrlShorteningService
, which is then stored in the database. We give the client access to the entire shortened URL.
public record ShortenUrlRequest(string Url);
app.MapPost("shorten", async (
ShortenUrlRequest request,
UrlShorteningService urlShorteningService,
ApplicationDbContext dbContext,
HttpContext httpContext) =>
{
if (!Uri.TryCreate(request.Url, UriKind.Absolute, out _))
{
return Results.BadRequest("The specified URL is invalid.");
}
var code = await urlShorteningService.GenerateUniqueCode();
var request = httpContext.Request;
var shortenedUrl = new ShortenedUrl
{
Id = Guid.NewGuid(),
LongUrl = request.Url,
Code = code,
ShortUrl = $"{request.Scheme}://{request.Host}/{code}",
CreatedOnUtc = DateTime.UtcNow
};
dbContext.ShortenedUrls.Add(shortenedUrl);
await dbContext.SaveChangesAsync();
return Results.Ok(shortenedUrl.ShortUrl);
});
Since we create a unique code first and then enter it into the database, there is a slight race condition here. Before our transaction is finished, a concurrent request might produce the same unique code and insert it into the database. I chose not to take on that case, though, because the likelihood of this occurring is minimal.
Recall that duplicate values are still prevented by the database’s unique index.
URL Redirection
Redirecting users who access a shortened URL is the second use case for URL shorteners.
For this feature, we’ll make another Minimal API endpoint available. After receiving a unique code, the endpoint will determine the appropriate shortened URL and send the user to the lengthy original URL. Before determining whether the specified code has a shortened URL in the database, you can apply extra validation.
app.MapGet("{code}", async (string code, ApplicationDbContext dbContext) =>
{
var shortenedUrl = await dbContext
.ShortenedUrls
.SingleOrDefaultAsync(s => s.Code == code);
if (shortenedUrl is null)
{
return Results.NotFound();
}
return Results.Redirect(shortenedUrl.LongUrl);
});
If the code is found in the database, this endpoint looks it up and sends the user back to the lengthy original URL. In accordance with HTTP standards, the response will have a 302 (Found) status code.
URL Shortener Improvement Points
Although our simple URL shortener works, there are a few things we could do better:
- Caching: Reduce database load for frequently visited URLs by implementing caching.
- Horizontal Scaling: Create a system that can scale horizontally to accommodate growing loads.
- Data Sharding: To disperse data among several databases, use data sharding.
- Analytics: Add analytics to monitor URL usage and provide users with reports.
- User Accounts: Permit users to manage their URLs by creating accounts.
The essential elements of creating a URL shortener with.NET have been discussed. You can build on this and use the enhancement points for a stronger solution.
If you are looking for fast and secure website, you can visit our site at https://www.asphostportal.com. Instead of hosting, you can purchase domain and SSL with us with an affordable price. Our fully featured hosting already includes
- Easy setup
- 24/7/365 technical support
- Top level speed and security
- Super cache server performance to increase your website speed
- Top 9 data centers across the world that you can choose.
Andriy Kravets is writer and experience .NET developer and like .NET for regular development. He likes to build cross-platform libraries/software with .NET.