Splitting DTO and Migration classes

I started using the migration class to manage my data model (it’s great, by the way).

Is it recommended to keep the ormlite database attributes like [Index], [Required], etc only on the private classes of the migration file only and the validation attributes on the dto?

Like so:

public class Migration1000 : MigrationBase
{
    private class Customer
    {
        [AutoIncrement]
        public int Id { get; set;}
        
        [Required]
        public string Name { get; set; }
        
        [Required]
        public CustomerStatus CustomerStatus { get; set; }
    }
    
    private enum CustomerStatus
    {
        Active,
        Disabled,
        Archived,
        Deleted,
        Unknown
    }
}

And the dto:

public class Customer
{
    public int Id { get; set;}
        
    [ValidateNotEmpty][ValidateNotNull]
    public string? Name { get; set; }
    
    [ValidateNotEmpty][ValidateNotNull]
    public CustomerStatus? CustomerStatus { get; set; }
}
    
public enum CustomerStatus
{
    Active,
    Disabled,
    Archived,
    Deleted,
    Unknown
}

[Tag("customers"), Description("Update an existing Customer")]
[Route("/customer/{Id}", "PATCH")]
[ValidateHasRole("Admin")]
public class UpdateCustomer : IPatchDb<Customer>, IReturn<IdResponse>
{
    public int Id { get; set; }
    
    public string? Name { get; set; }

    public CustomerStatus? TenantPbxStatus { get; set; }
}

On the dto, do you recommend making all the properties nullable by default and add validation?

For example, if I want to use Patch with the ormlite api so that in my service I can have:

public object Patch(UpdateCustomer req)
{
    var customer = req.ConvertTo<Customer>();
    
    int count = Db.UpdateNonDefaults(customer, x => x.Id == req.Id);

    //throw if nothing updated

    return new CustomerResponse();
}

Since the default can be used in a patch operation for Enum and potentially other properties, I guess it’s better to make all the properties nullable on the UpdateCustomer class? Otherwise, in my example, Active is the default if the property is non nullable and patching to Active would not work. I’m not sure ConvertTo is intended to be use like this.

@Math your migration classes are frozen in time versions of the model classes you use in your application. Some of the attributes (like Required) do impact other functionality, so you will want to replicate them to match rather than make them different.

The reason they are private and separate is so they are a snapshot of the model class at that point in time the changes in the specific migration class was required. As the requirements of your application change, new Migration10xx classes should be created representing that diff and migration step to keep up with your application model class.

For an operational like PATCH, likely all nullable would suit, but again, it will depend on your API requirements. If you have a specific API using PATCH that still has things like dependent properties or multiple required properties, it would make sense to make just some optional, just like how Id isn’t nullable since the resource being patched needs to be identified.

Generally on something like a PATCH verb you will likely (but not strictly always) want to just make it idempotent, so throwing an error if nothing is updated might be unexpected and unnecessary friction for the client. Eg, a client can safely send the same payload N times and it will result in the same state.

Best to always have tests around services with heavy use of ConvertTo and UpdateNonDetaults if you have different nullable vs non-nullable properties between types to ensure the behavior is indeed what you expected.

Hope that helps.

1 Like

Very good.

What kind of functionality is impacted by Required ?

Would you also duplicate for instance:

[ForeignKey(typeof(Customer))]

Outside of the migration class ?

Is ServiceStack using those ForgeignKey attribute for other features ?

I was under the impression they were targeting the db only.

They are targetting DB, but if your models are being used/returned via your APIs, then the metadata ServiceStack generates for features like Locode will be changed.

Eg, you would:

  • When creating a new table, your model and migration classes would be duplicated, Migration1000.cs
  • Adding a new column, you might use the Db.Migrate<T>, so your migration class would only have the new column declared.
// Migration1000.cs
public class Migration1000 : MigrationBase
{
    class Booking : AuditBase
    {
        [AutoIncrement]
        public int Id { get; set; }
        public string Name { get; set; }
        public RoomType RoomType { get; set; }
        public int RoomNumber { get; set; }
        public DateTime BookingStartDate { get; set; }
        public DateTime? BookingEndDate { get; set; }
        public decimal Cost { get; set; }
        public string? Notes { get; set; }
        public bool? Cancelled { get; set; }
        
        [References(typeof(Coupon))]
        public string? CouponId { get; set; }
    }
...

Then adding a new column in Migration1001.cs, note that Migration1000.cs is no longer touched, we don’t alter it, it is there as a step in the migration process going from zero to your current application database schema that your application requires.

// Migration1001.cs
public class Migration1001 : MigrationBase
{
    class Booking
    {
         public int? MyNewColumn {get;set;}
    }

    public override Up()
    {
         Db.Migrate<Booking>();
    }
      

Your applications Booking class will be a single class that represents the current latest used version of the database model. They represent the same thing.

// Booking.cs in ServiceModel project
public class Booking : AuditBase
{
    [AutoIncrement]
    public int Id { get; set; }
    public string Name { get; set; }
    public RoomType RoomType { get; set; }
    public int RoomNumber { get; set; }
    public DateTime BookingStartDate { get; set; }
    public DateTime? BookingEndDate { get; set; }
    public decimal Cost { get; set; }
    public string? Notes { get; set; }
    public bool? Cancelled { get; set; }
    
    [References(typeof(Coupon))]
    public string? CouponId { get; set; }

    public int? MyNewColumn {get;set;}
}

An example of usage of a lot of these is to populate the AppMetadata which can be used by ServiceStack Clients (libraries, Locode/built in UIs etc) for additional functionality by inferring usage by these attributes. But also OrmLite will also use this information via GetModelDefinition.

While each class represents the same thing, they live in isolation, the inner Migration classes are just there to represent the tables as they are built up and changed during initializing/migration database changes, where as your application uses them for CRUD operations via OrmLite etc. You can use the MigrationBase classes with Up without using these inner classes at all and just execute plain SQL, Db.Migrate<T>,Db.Revert<T> and schema operations via OrmLite are just handy ways to achieving the same outcome.

So your migration private class and application class live in isolation, so should be treated as representing the same thing with different roles, one is a structure snapshot over time (migrations), the other your living/current application for interacting with your database.

As for your Request DTOs like CreateBooking or UpdateBooking via PATCH, no you don’t use the OrmLite attributes like ForeignKeyAttribute etc, but to clarify, your database model for OrmLite/usage in your application is completely independent from a model class in your migration process.

Let me know if anything is still unclear, and hopefully will be able to clear it up for you.

Excellent. Thank you for taking the time to explain it. It’s clearer now.

1 Like