
Posted on • Originally published atmilanjovanovic.tech on
How to Build a URL Shortener With .NET
AURL shortener is a simple yet powerful tool that converts long URLs into more manageable, shorter versions. This is particularly useful for sharing links on platforms with character limits or improving user experience by reducing clutter. Two popular URL shorteners areBitly andTinyURL. Designing a URL shortener is an interesting challenge with fun problems to solve.
But how would you build a URL shortener in .NET?
URL shorteners have two core functionalities:
- Generating a unique code for a given URL
- Redirecting users who access the short link to the original URL
Today, I'll guide you through the design, implementation, and considerations for creating your URL shortener.
URL Shortener System Design
Here's the high-level system design for our URL shortener. We want to expose two endpoints. One to shorten a long URL and the other to redirect users based on a shortened URL. The shortened URLs are stored in aPostgreSQL database in this example. We can introduce a distributed cache likeRedis to the system to improve read performance.
We first need to ensure a large number of short URLs. We're going to assign a unique code to each long URL, and use it to generate the shortened URL. The unique code length and set of characters determine how many short URLs the system can generate. We will discuss this in more detail when we implement unique code generation.
We're going to use the random code generation strategy. It's straightforward to implement and has an acceptably low rate of collisions. The trade-off we're making is increased latency, but we will also explore other options.
The Data Model
Let's start by figuring out what we will store in the database. Our data model is straightforward. We have aShortenedUrl
class representing the URLs stored in our system:
publicclassShortenedUrl{publicGuidId{get;set;}publicstringLongUrl{get;set;}=string.Empty;publicstringShortUrl{get;set;}=string.Empty;publicstringCode{get;set;}=string.Empty;publicDateTimeCreatedOnUtc{get;set;}}
This class includes properties for the original URL (LongUrl
), the shortened URL (ShortUrl
), and a unique code (Code
) that represents the shortened URL. TheId
andCreatedOnUtc
fields are used for database and tracking purposes. The users will send the uniqueCode
to our system, which will try to find a matchingLongUrl
and redirect them.
In addition, we will also define an EFApplicationDbContext
class, which is responsible for configuring our entity and setting up our database context. I'm doing two things here to improve performance:
- Configuring the
Code
maximum length withHasMaxLength
- Defining a unique index on the
Code
column
A unique index shields us from concurrency conflicts, so we will never have duplicateCode
values persisted in the database. Setting the maximum length for this column saves storage space, and it's a requirement for indexing string columns in some databases.
Note that some databases treat strings in a case-insensitive way. This severely reduces the number of available short URLs. You want to configure the database to treat the unique code in a case-sensitive way.
publicclassApplicationDbContext:DbContext{publicApplicationDbContext(DbContextOptionsoptions):base(options){}publicDbSet<ShortenedUrl>ShortenedUrls{get;set;}protectedoverridevoidOnModelCreating(ModelBuildermodelBuilder){modelBuilder.Entity<ShortenedUrl>(builder=>{builder.Property(shortenedUrl=>shortenedUrl.Code).HasMaxLength(ShortLinkSettings.Length);builder.HasIndex(shortenedUrl=>shortenedUrl.Code).IsUnique();});}}
Unique Code Generation
The most crucial part of our URL shortener is generating a unique code for each URL. There are a few different algorithms you can choose to implement this. We want an even distribution of unique codes across all possible values. This helps to reduce potential collisions.
I will implement a random, unique code generator with a predefined alphabet. It's simple to implement, and the chance of collision is relatively low. Still, there are more performant solutions than this, but more on this later.
Let's define aShortLinkSettings
class that contains two constants. One is for defining the length of the unqualified code we will generate. The other constant is the alphabet we will use to generate the random code.
publicstaticclassShortLinkSettings{publicconstintLength=7;publicconststringAlphabet="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";}
The alphabet has62
characters, which gives us62^7
possible unique code combinations.
If you're wondering, this is3,521,614,606,208
combinations.
Spelled out: three trillion, five hundred twenty-one billion, six hundred fourteen million, six hundred six thousand, two hundred eight.
Those are quite a few unique codes, which will be enough for our URL shortener.
Now, let's implement ourUrlShorteningService
, which handles generating unique codes. This service generates a random string of the specified length using our predefined alphabet. It checks against the database to ensure uniqueness.
publicclassUrlShorteningService(ApplicationDbContextdbContext){privatereadonlyRandom_random=new();publicasyncTask<string>GenerateUniqueCode(){varcodeChars=newchar[ShortLinkSettings.Length];constintmaxValue=ShortLinkSettings.Alphabet.Length;while(true){for(vari=0;i<ShortLinkSettings.Length;i++){varrandomIndex=_random.Next(maxValue);codeChars[i]=ShortLinkSettings.Alphabet[randomIndex];}varcode=newstring(codeChars);if(!awaitdbContext.ShortenedUrls.AnyAsync(s=>s.Code==code)){returncode;}}}}
Downsides and Improvement Points
The downside of this implementation is increased latency because we're checking each code we generate against the database. An improvement point could be generating the unique codes in the database ahead of time.
Another improvement point could be using a fixed number of iterations instead of an infinite loop. In case of multiple collisions in a row, the current implementation would continue until a unique value is found. Consider throwing an exception instead after a few collisions in a row.
URL Shortening
Now that our core business logic is ready, we can expose an endpoint to shorten URLs. We can use a simple Minimal API endpoint.
This endpoint accepts a URL, validates it, and then uses theUrlShorteningService
to create a shortened URL, which is then saved to the database. We return the full shortened URL to the client.
publicrecordShortenUrlRequest(stringUrl);app.MapPost("shorten",async(ShortenUrlRequestrequest,UrlShorteningServiceurlShorteningService,ApplicationDbContextdbContext,HttpContexthttpContext)=>{if(!Uri.TryCreate(request.Url,UriKind.Absolute,out_)){returnResults.BadRequest("The specified URL is invalid.");}varcode=awaiturlShorteningService.GenerateUniqueCode();varrequest=httpContext.Request;varshortenedUrl=newShortenedUrl{Id=Guid.NewGuid(),LongUrl=request.Url,Code=code,ShortUrl=$"{request.Scheme}://{request.Host}/{code}",CreatedOnUtc=DateTime.UtcNow};dbContext.ShortenedUrls.Add(shortenedUrl);awaitdbContext.SaveChangesAsync();returnResults.Ok(shortenedUrl.ShortUrl);});
There is a minorrace condition here, as we generate a unique code first and then insert it into the database. A concurrent request could generate the same unique code and insert it into the database before we complete our transaction. However, the chances of this happening are low, so I decided not to handle that case.
Remember that the unique index in the database is still guarding us against duplicate values.
URL Redirection
The second use case for a URL shortener is redirection when accessing a shortened URL.
We will expose another Minimal API endpoint for this feature. The endpoint will accept a unique code, find the respective shortened URL, and redirect the user to the original long URL. You can implement additional validation for the specified code before checking if there's a shortened URL in the database.
app.MapGet("{code}",async(stringcode,ApplicationDbContextdbContext)=>{varshortenedUrl=awaitdbContext.ShortenedUrls.SingleOrDefaultAsync(s=>s.Code==code);if(shortenedUrlisnull){returnResults.NotFound();}returnResults.Redirect(shortenedUrl.LongUrl);});
This endpoint looks up the code in the database and, if found, redirects the user to the original long URL. The response will have a302 (Found) status code per the HTTP standards.
URL Shortener Improvement Points
While our basic URL shortener is functional, there are several areas we can improve:
- Caching : Implement caching to reduce database load for frequently accessed URLs.
- Horizontal Scaling : Design the system to scale horizontally to handle increased load.
- Data Sharding : Implement data sharding to distribute data across multiple databases.
- Analytics : Introduce analytics to track URL usage and expose reports to users.
- User Accounts : Allow users to create accounts to manage their URLs.
We've covered the key components of building a URL shortener using .NET. You can take this further and implement the improvements points for a more robust solution.
If you want to see me build this from scratch, here's avideo tutorial on YouTube.
That's all for today.
See you next week.
P.S. Whenever you're ready, there are 2 ways I can help you:
Pragmatic Clean Architecture: This comprehensive course will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.Join 2,200+ students here.
Patreon Community: Think like a senior software engineer withaccess to the source code I use in my YouTube videos andexclusive discounts for my courses.Join 1,000+ engineers here.
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse