Resource identification
To identify a resource from a public interface:
- Use GUIDs
- Use slugs (lowercase version of the name with hyphens replacing spaces)
- Do not use sequential integers
Slugs are an ideal choice for items such as locations, resource types where the guessability of the name is not a problem and in many cases can be useful.
GET /api/locations/london/details
Instead of:
GET /api/locations/1/details
Use GUIDs for any resource where it is important to prevent people from easily changing the number in order to find another instance. Examples would include any user identifier and transaction Ids.
GET /api/user/511AC810-C968-4CE0-8588-01125B551088/details
Sequential integers still have significant performance benefits as the PK/FK on SQL table joins so should be used internally, but they pose a security weakness if used for external resource identification.
Use an abstract ‘Entity’ class
One way to enforce this is to use a base class to setup all Entities in the same way.
// This uses a primary constructor, available from .Net8
public abstract class Entity<T>(T id) where T : ValueObject
{
public T Id { get; protected set; } = id;
public bool IsActive { get; protected set; } = true;
public bool IsDeleted { get; protected set; } = false;
}
This uses a strongly typed ID to avoid problems with the order being confused in methods referencing multiple IDs.
When you want to support a slug for the entity, then add another abstract class which inherits from this one:
public abstract class EntityWithSlug<T> : Entity<T> where T : ValueObject
{
protected EntityWithSlug(T id, string name) : base(id)
{
Name = name;
if (!string.IsNullOrWhiteSpace(name))
{
var slugResult = Slug.Create(name);
Slug = slugResult.IsSuccess ? slugResult.Value : null;
}
}
[MaxLength(50)]
public string Name { get; protected set; }
public Slug? Slug { get; protected set; }
}
When used in a class, the ID needs to be passed to the base class:
public sealed class Booking : Entity<BookingId>
{
private Booking(BookingId id) : base(id)
{
Status = BookingStatus.Provisional;
Source = BookingSource.CallHandler;
}
public Appointment? Appointment { get; private set; }
public BookingStatus Status { get; private set; }
public BookingSource Source { get; private set; }
public DateTime Created { get; private set; } = DateTime.UtcNow;
// other properties and methods
BookingId is created as a value object. Note the domain object for Booking doesn’t need to hold the ID of the Appointment, it just has the navigation property to appointment.
Configuration for Entity Framework
internal class BookingConfiguration : IEntityTypeConfiguration<Booking>
{
public void Configure(EntityTypeBuilder<Booking> builder)
{
builder.ToTable("Bookings");
// Creates the integer primary key
builder.Property<int>("BookingId").IsRequired();
builder.HasKey("BookingId");
// Tells entity how to retrieve and set the value for the ID
builder.Property(b => b.Id)
.HasConversion(b => b.Value, b => BookingId.Create(b));
// Sets up the navigation to the Appointment
builder.Navigation(b => b.Appointment)
.UsePropertyAccessMode(PropertyAccessMode.Property)
.AutoInclude();
}
}