F# type providers vs C# interfaces + Entity Framew

2020-08-10 09:49发布

问题:

The question is very technical, and it sits deeply between F# / C# differences. It is quite likely that I might’ve missed something. If you find a conceptual error, please, comment and I will update the question.

Let’s start from C# world. Suppose that I have a simple business object, call it Person (but, please, keep in mind that there are 100+ objects far more complicated than that in the business domain that we work with):

public class Person : IPerson
{
    public int PersonId { get; set; }
    public string Name { get; set; }
    public string LastName { get; set; }
}

and I use DI / IOC and so that I never actually pass a Person around. Rather, I would always use an interface (mentioned above), call it IPerson:

public interface IPerson
{
    int PersonId { get; set; }
    string Name { get; set; }
    string LastName { get; set; }
}

The business requirement is that the person can be serialized to / deserialized from the database. Let’s say that I choose to use Entity Framework for that, but the actual implementation seems irrelevant to the question. At this point I have an option to introduce “database” related class(es), e.g. EFPerson:

public class EFPerson : IPerson
{
    public int PersonId { get; set; }
    public string Name { get; set; }
    public string LastName { get; set; }
}

along with the relevant database related attributes and code, which I will skip for brevity, and then use Reflection to copy properties of IPerson interface between Person and EFPerson OR just use EFPerson (passed as IPerson) directly OR do something else. This is fairly irrelevant, as the consumers will always see IPerson and so the implementation can be changed at any time without the consumers knowing anything about it.

If I need to add a property, then I would update the interface IPerson first (let’s say I add a property DateTime DateOfBirth { get; set; }) and then the compiler will tell me what to fix. However, if I remove the property from the interface (let’s say that I no longer need LastName), then the compiler won’t help me. However, I can write a Reflection-based test, which would ensure that the properties of IPerson, Person, EFPerson, etc. are identical. This is not really needed, but it can be done and then it will work like magic (and yes, we do have such tests and they do work like magic).

Now, let’s get to F# world. Here we have the type providers, which completely remove the need to create database objects in the code: they are created automatically by the type providers!

Cool! But is it?

First, somebody has to create / update the database objects and if there is more than one developer involved, then it is natural that the database may and will be upgraded / downgraded in different branches. So far, from my experience, this is an extreme pain on the neck when F# type providers are involved. Even if C# EF Code First is used to handle migrations, some “extensive shaman dancing” is required to make F# type providers “happy”.

Second, everything is immutable in F# world by default (unless we make it mutable), so we clearly don’t want to pass mutable database objects upstream. Which means that once we load a mutable row from the database, we want to convert it into a “native” F# immutable structure as soon as possible so that to work only with pure functions upstream. After all, using pure functions decreases the number of required tests in, I guess, 5 – 50 times, depending on the domain.

Let’s get back to our Person. I will skip any possible re-mapping for now (e.g. database integer into F# DU case and similar stuff). So, our F# Person would look like that:

type Person =
    {
        personId : int
        name : string
        lastName : string
    }

So, if “tomorrow” I need to add dateOfBirth : DateTime to this type, then the compiler will tell me about all places where this needs to be fixed. This is great because C# compiler will not tell me where I need to add that date of birth, … except the database. The F# compiler will not tell me that I need to go and add a database column to the table Person. However, in C#, since I would have to update the interface first, the compiler will tell me which objects must be fixed, including the database one(s).

Apparently, I want the best from both worlds in F#. And while this can be achieved using interfaces, it just does not feel the F# way. After all, the analog of DI / IOC is done very differently in F# and it is usually achieved by passing functions rather than interfaces.

So, here are two questions.

  1. How can I easily manage database up / down migrations in F# world? And, to start from, what is the proper way to actually do the database migrations in F# world when many developers are involved?

  2. What is the F# way to achieve “the best of C# world” as described above: when I update F# type Person and then fix all places where I need to add / remove properties to the record, what would be the most appropriate F# way to “fail” either at compile time or at least at test time when I have not updated the database to match the business object(s)?

回答1:

How can I easily manage database up / down migrations in F# world? And, to start from, what is the proper way to actually do the database migrations in F# world when many developers are involved?

Most natural way to manage Db migrations is to use tools native to db i.e. plain SQL. At our team we use dbup package, for every solution we create a small console project to roll up db migrations in dev and during deployment. Consumer apps are both in F# (type providers) and C# (EF), sometimes with the same database. Works like a charm.

You mentioned EF Code First. F# SQL providers are all inherently "Db First" because they generate types based on external data source (database) and not the other way around. I don't think that mixing two approaches is a good idea. In fact I wouldn't recommend EF Code First to anyone to manage migrations: plain SQL is simpler, doesn't require "extensive shaman dancing", infinitely more flexible and understood by far more people. If you are uncomfortable with manual SQL scripting and consider EF Code First just for automatic generation of migration script then even MS SQL Server Management Studio designer can generate migration scripts for you

What is the F# way to achieve “the best of C# world” as described above: when I update F# type Person and then fix all places where I need to add / remove properties to the record, what would be the most appropriate F# way to “fail” either at compile time or at least at test time when I have not updated the database to match the business object(s)?

My recipe is as follows:

  • Don't use the interfaces. as you said :)

interfaces, it just does not feel the F# way

  • Don't let autogenerated types from type provider to leak outside thin db access layer. They are not business objects, and neither EF entities are as a matter of fact.
  • Instead declare F# records and/or discriminated unions as your domain objects. Model them as you please and don't feel constrained by db schema.
  • In db access layer, map from autogenerated db types to your domain F# types. Every usage of types autogenerated by Type Provider begins and ends here. Yes, it means you have to write mappings manually and introduce human factor here e.g. you can accidentally map FirstName to LastName. In practice it's a tiny overhead and benefits of decoupling outweigh it by a magnitude.
  • How to make sure you don't forget to map some property? It's impossible, F# compiler will emit error if record not fully initialized.
  • How to add new property and not forget to initialize it? Start with F# code: add new property to domain record/records, F# compiler will guide you to all record instantiations (usually just one) and force you to initialize it with something (you will have to add a migration script / upgrade database schema accordingly).
  • How to remove a property and don't forget to clean up everything up to db schema. Start from the other end: delete column from database. All mappings between type provider types and domain F# records will break and highlight properties that became redundant (more importantly, it will force you to double check that they are really redundant and reconsider your decision).
  • In fact in some scenarios you may want to preserve database column (e.g. for historical/audit purposes) and only remove property from F# code. It's just one (and rather rare) of multitude of scenarios when it's convenient to have domain model decoupled from db schema.

In Short

  • migrations via plain SQL
  • domain types are manually declared F# records
  • manual mapping from Type Providers to F# domain types

Even Shorter

Stick with Single Responsibility Principle and enjoy the benefits.