F#类型提供程序与C#接口+实体框架

时间:2019-01-21 00:49:04

标签: c# entity-framework f# type-providers

这个问题是非常技术性的,它深深地介于F#/ C#之间。我很可能错过了一些东西。如果您发现概念错误,请发表评论,我将更新问题。

让我们从C#世界开始。假设我有一个简单的业务对象,将其称为Person(但是请记住,有100多个对象远比我们使用的业务领域中的对象复杂):

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

,并且我使用DI / IOC,所以我实际上从未传递过Person。相反,我将始终使用接口(如上所述),将其称为IPerson

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

业务需求是可以将该人序列化到数据库或从数据库反序列化。假设我选择为此使用Entity Framework,但实际的实现似乎与问题无关。在这一点上,我可以选择引入与“数据库”相关的类,例如EFPerson

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

连同相关的数据库相关属性和代码,为了简洁起见,我将跳过这些,然后使用Reflection在IPersonPerson之间复制EFPerson接口的属性,或者仅使用{ {1}}(作为EFPerson传递)直接执行其他操作。这是完全无关紧要的,因为消费者将始终看到IPerson,因此可以在不了解消费者任何情况的情况下随时更改实现。

如果我需要添加属性,那么我将首先更新接口IPerson(假设我添加了属性IPerson),然后编译器会告诉我要解决的问题。但是,如果我从界面中删除了该属性(假设我不再需要DateTime DateOfBirth { get; set; }),那么编译器将无济于事。但是,我可以编写一个基于反射的测试,该测试将确保LastNameIPersonPerson等的属性相同。确实不是必需的,但是可以做到这一点,然后它将像魔术一样工作(是的,我们确实有这样的测试,它们确实像魔术一样工作。)

现在,让我们进入F#世界。这里我们有类型提供程序,它完全不需要在代码中创建数据库对象:它们是由类型提供程序自动创建的!

酷!但是吗?

首先,必须有人创建/更新数据库对象,如果涉及的开发人员不止一个,那么很自然地可以并且将在不同分支中升级/降级数据库。到目前为止,根据我的经验,当涉及到F#类型提供程序时,这是一个极大的痛苦。即使使用C#EF Code First来处理迁移,也需要一些“大量的萨满舞”才能使F#类型的提供者“高兴”。

第二,默认情况下,F#世界中的所有内容都是不可变的(除非我们使其可变),因此我们显然不希望将可变数据库对象传递给上游。这意味着一旦我们从数据库加载了可变行,就希望尽快将其转换为“本地” F#不可变结构,以便仅在上游使用纯函数。毕竟,根据域的不同,使用纯函数会减少5至50倍所需的测试次数。

让我们回到我们的EFPerson。我现在将跳过任何可能的重新映射(例如,将数据库整数转换为F#DU大小写和类似内容)。因此,我们的F#Person如下所示:

Person

因此,如果“明天”我需要在此类型上添加type Person = { personId : int name : string lastName : string } ,则编译器将告诉我所有需要修复此问题的地方。这很棒,因为C#编译器不会告诉我除数据库之外我需要在哪里添加该出生日期。 F#编译器不会告诉我需要去往表dateOfBirth : DateTime中添加数据库列。但是,在C#中,由于我必须先更新接口,因此编译器会告诉我必须修复哪些对象,包括数据库一个。

显然,我想要F#中的两全其美。尽管可以使用接口来实现这一点,但是感觉不到F#的方式。毕竟,DI / IOC的模拟在F#中的实现方式非常不同,通常是通过传递函数而不是接口来实现的。

所以,这是两个问题。

  1. 如何在F#世界中轻松管理数据库的上/下迁移?从头开始,当涉及到许多开发人员时,在F#世界中实际进行数据库迁移的正确方法是什么?

  2. 如上所述,实现“最佳C#世界”的F#方法是什么:当我更新F#时,键入Person,然后修复所有需要在记录中添加/删除属性的地方,在我没有更新数据库以匹配业务对象时,在编译时或至少在测试时“失败”的最合适的F#方法是什么?

1 个答案:

答案 0 :(得分:3)

  

如何在F#世界中轻松管理数据库上/下迁移?   并且,首先,实际做数据库的正确方法是什么   涉及许多开发人员时在F#世界中迁移?

管理Db迁移的最自然的方法是使用数据库固有的工具,即普通SQL。在我们的团队中,我们使用dbup软件包,对于每个解决方案,我们都会创建一个小型控制台项目,以在开发和部署期间汇总数据库迁移。消费者应用程序同时在F#(类型提供程序)和C#(EF)中,有时具有相同的数据库。奇迹般有效。

您首先提到了EF代码。 F#SQL提供程序本质上都是“ Db First”,因为它们基于外部数据源(数据库)生成类型,而不是基于其他方式。我认为混合两种方法不是一个好主意。实际上,我不会向任何人推荐EF Code First来管理迁移:纯SQL更简单,不需要“广泛的萨满舞”,它无限灵活并且为更多的人所了解。 如果您对手动SQL脚本不满意,并考虑将EF Code First仅仅用于自动生成迁移脚本,那么甚至可以使用MS SQL Server Management Studio designer can generate migration scripts for you

  

实现上述“最佳C#世界”的F#方法是什么   上图:当我更新F#时,键入Person,然后修复我所有的位置   需要在记录中添加/删除属性,最多的是   在编译时或至少在编译时“失败”的适当F#方式   我没有更新数据库以匹配业务时的测试时间   个对象?

我的食谱如下:

  • 请勿使用界面。如你所说:)
  

界面,只是感觉不到F#方式

  • 不要让来自类型提供程序的自动生成的类型泄漏到瘦数据库访问层之外。它们不是业务对象,事实上neither EF entities are
  • 改为声明F#记录和/或已区分的联合作为您的域对象。随便对它们进行建模,而不会受到数据库模式的约束。
  • 在数据库访问层中,将自动生成的数据库类型映射到您的域F#类型。 由类型提供程序自动生成的每种类型的使用都在此处开始和结束。是的,这意味着您必须手动编写映射并在此处引入人为因素,例如您可能不小心将FirstName映射到LastName。实际上,这是一个很小的开销,去耦的好处要大得多。
  • 如何确保您不会忘记映射某些属性?不可能,如果记录未完全初始化,F#编译器将发出错误。
  • 如何添加新属性,而不忘记对其进行初始化?从F#代码开始:向域记录/记录添加新属性,F#编译器将指导您进行所有记录实例化(通常只有一个实例),并强制您使用某种方式对其进行初始化(您将必须相应地添加迁移脚本/升级数据库架构) )。
  • 如何删除属性,并且别忘了清理数据库结构中的所有内容。从另一端开始:从数据库中删除列。类型提供程序类型和域F#记录之间的所有映射都将中断并突出显示成为冗余的属性(更重要的是,这将迫使您再次检查它们是否确实是冗余的,并重新考虑您的决定)。
  • 实际上,在某些情况下,您可能想保留数据库列(例如,出于历史/审计目的),而仅从F#代码中删除属性。将域模型与db模式分离很方便时,这只是多种情况中的一种(而且很少见)。

简而言之

  • 通过普通SQL迁移
  • 域类型是手动声明的F#记录
  • 从类型提供程序到F#域类型的手动映射

更短

坚持单一责任原则,享受好处。