[翻译] 基于.NET Core构建微服务 第五部分:Marten域聚合的理想仓库

原文:Building Microservices On .NET Core – Part 5 Marten An Ideal Repository For Your Domain Aggregates
作者:Wojciech Suwała, Head Architect, ASC LAB
时间:2019年4月11日

这是我们系列中有关在.NET Core上构建微服务的第五篇文章。 在第一篇文章中,我们介绍了该系列并准备了计划:业务案例和解决方案体系结构。 在第二篇文章中,我们描述了如何使用CQRS模式和MediatR库来构建一个微服务的内部架构。 在第三篇文章中,我们描述了服务发现在基于微服务的体系结构中的重要性,并介绍了Eureka的实际实现。 在第四部分中,我们介绍了如何使用Ocelot为微服务构建API网关。

在本文中,我们将退一步,讨论数据访问以及如何有效地持久存储数据。

完整解决方案的源代码可以在我们的GitHub上找到。

持久性是一个已解决的问题,不是吗?

当.NET Framework的第一个版本于2002年左右发布时,我们有两个用于数据访问的主要API:数据集和数据读取器。数据集,在数据库中表的内存表示形式中,另一方面,数据读取器答应让您快速读取数据,但必须手动将其推入对象中。许多开发人员发现了反射的力量,几乎每个人都开发了自己的ORM。过去,我的团队很少评估这样的框架,但是对于我们来说,这些框架似乎都不是合适的解决方案,因为我们正在为保险业开发复杂的应用程序。因此,我们决定在每次插入,更新和搜索时都使用DataReaders和手工编码的SQL。几年后,我们建立了今天称为微型ORM的产品。即使使用我们自己开发的简单工具,我们也可以消除大约70%的数据访问代码。然后是NHibernate时代。作为具有Java经验的开发人员,我很嫉妒Java同事拥有如此强大的库,当NHibernate的早期版本可用时,我很想尝试一下。当我们开始在生产中使用NHibernate时,我认为它是2.0版。多年来,NHibernate一直是我们的首选,并在许多项目中为我们提供了帮助。这是令人难以置信的灵活且功能丰富的库。但是微软决定实施自己的专有解决方案–实体框架。随着它的大力推广,许多.NET商店决定改用EF和NHibernate,因此社区开始萎缩。

然后,Microsoft引入了.NET Core,一切都变了。他们没有移植现有的实体框架,而是决定从头开始开发它的新版本。结果,.NET Core的第一个版本实际上没有用于数据访问的企业级解决方案。当前,我们几乎已经拥有.NET Core 3,而EF Core仍然缺少您希望成熟的ORM提供的许多功能。 NHibernate最终登陆.NET Core,但我认为它不会因为周围的社区小得多而重新流行起来。与今天的ORM相比,NHibernate也具有很大的侵入性,例如,它迫使您将所有属性虚拟化,因此可以由ORM代理。

.NET Core的到来和微服务的日益普及完全改变了.NET体系结构的格局。您现在可以在Linux上进行开发和部署。在.NET开发人员中,MS SQL以外的数据库的使用正变得越来越流行。

微服务还增加了多语言持久性流行度。开发人员意识到他们可以将不同的数据存储用于不同种类的服务。有文档数据库,图形数据库,事件存储和其他类型的数据库相关解决方案。

如您所见,有很多选项可供选择,在本文中,我想谈一谈使用关系数据库作为文档数据库,以充分利用两者的优势。在Marten的帮助下,您可以实现这一目标。

什么是Marten?

Marten是一个客户端库,允许.NET开发人员将Postgresql用作文档数据库和事件存储。它由杰里米·米勒(Jeremy Miller)于2015年10月左右某个时候启动,以替代RavenDB数据库,但不仅限于此。

如果您曾经使用过像MongoDB或RavenDB这样的文档数据库,您就会知道它为您带来了出色的开发人员体验,尤其是易用性和开发速度,但是还存在一些与性能和数据一致性相关的问题。

使用Marten,您可以轻松地将关系数据库用作文档之一,并具有完全的ACID合规性和广泛的LINQ支持。

从我的角度来看,对于这种方法,有一个特定的用例似乎是理想的。如果您正在练习域驱动的设计并将域模型划分为小的聚合,则可以将聚合视为文档。如果您采用这种方法,并与Marten之类的库结合使用,则持久化,加载和查找聚合几乎不需要任何代码。由于符合ACID,因此您可以在同一事务中修改和保存许多聚合,而这在许多文档数据库中是不可能的。使用关系数据库还可以简化基础架构管理,因为您仍然可以依靠熟悉的工具进行备份和监视。

更重要的是,您的域模型不受ORM功能的限制。

Vaughn Vernon的文章“理想的域驱动设计聚合存储”中描述了将聚合作为JSON保存在关系数据库中的想法,从中我从中获得了这篇文章标题的灵感。

使用Marten

将Marten添加到项目

与往常一样,我们首先使用NuGet将Marten依赖项添加到我们的项目中。

Install-Package Marten

接下来需要做的是将连接字符串添加到PostgreSQL数据库中的appsettings.json

{
    "ConnectionStrings": {
        "PgConnection": "User ID=lab_user;Password=*****;Database=lab_netmicro_payments;Host=localhost;Port=5432"
    }}

我们还需要安装PostgreSQL 9.5+数据库服务器。

设置Marten

现在我们可以建立Marten。 我们将看一下取自PaymentService的示例代码。
在我们的解决方案中,我们决定将域逻辑与持久性细节分开,为此我们引入了两个接口。

public interface IPolicyAccountRepository
{
    void Add(PolicyAccount policyAccount);

    Task FindByNumber(string accountNumber);
}

第一个接口代表PolicyAccount聚合的存储库。 在这里,我们使用仓库模式,如Eric Evans的DDD Blue Book中所述。 我们的存储库提供了用于存储数据的接口,因此我们可以将其用作简单的收集类。 请注意,我们不创建通用存储库。 如果我们正在进行域驱动的设计,则存储库应该是域语言的一部分,并且应该仅公开域代码所需的操作。

第二个界面表示工作单元模式(Unit Of Work Pattern)–一种服务,该服务跟踪加载的对象并让我们保留更改。

public interface IDataStore : IDisposable
    {
        IPolicyAccountRepository PolicyAccounts { get; }

        Task CommitChanges();
    }

数据存储接口使我们能够访问存储库,因此我们可以从存储库中添加和检索策略帐户对象,并允许我们将更改提交到持久性存储。

让我们看看如何使用Marten来实现这些接口。但是在开始之前,我们必须了解Marten中的两个基本概念:DocumentStoreDocumentSession

第一个代表我们文档存储的配置。它保留配置数据,例如连接字符串,序列化设置,模式定制,映射信息。

DocumentSession代表我们的工作单元。它负责打开和管理数据库连接,针对数据库执行SQL语句,加载文档,跟踪加载的文档以及最后将更改保存回数据库。首先,创建一个DocumentStore实例,然后可以要求它创建一个DocumentSession实例,最后可以使用DocumentSession在数据库中创建,加载,修改和存储文档。

DocumentSession有三种实现:

  • 轻量级会话 – 不跟踪更改的会话,
  • 标准会话 – 具有身份映射跟踪的会话,但没有更改跟踪
  • 脏跟踪会话 – 一种带有身份映射和脏跟踪的会话。

脏跟踪是通过比较最初从数据库加载的JSON和从聚合生成的JSON来实现的,因此您必须了解性能和内存成本。在我们的代码中,我们将使用轻量级文档会话。

您可以从Marten的官方文档中了解更多信息。

现在我们知道了基础知识。我们可以创建MartenInstaller类,将在Startup类中使用该类来初始化和连接所有必需的片段。

public static class MartenInstaller
    {
        public static void AddMarten(this IServiceCollection services, string cnnString)
        {
            services.AddSingleton(CreateDocumentStore(cnnString));

            services.AddScoped();
        }

        private static IDocumentStore CreateDocumentStore(string cn)
        {
            return DocumentStore.For(_ =>
            {
                _.Connection(cn);
                _.DatabaseSchemaName = "payment_service";
                _.Serializer(CustomizeJsonSerializer());
                _.Schema.For().Duplicate(t => t.PolicyNumber,pgType: "varchar(50)", configure: idx => idx.IsUnique = true);
            });
        }

        private static JsonNetSerializer CustomizeJsonSerializer()
        {
            var serializer = new JsonNetSerializer();

            serializer.Customize(_ =>
            {
                _.ContractResolver = new ProtectedSettersContractResolver();
        _.PreserveReferencesHandling = PreserveReferencesHandling.Objects;
            });

            return serializer;
        }
    }

此处的关键方法是CreateDocumentStore。 它创建一个文档存储实例并对其进行配置。

DocumentStore.For(_ =>
            {
                _.Connection(cn); (1)
                _.DatabaseSchemaName = "payment_service"; (2)
                _.Serializer(CustomizeJsonSerializer()); (3)
                _.Schema.For().Duplicate(t => t.PolicyNumber,pgType: "varchar(50)", configure: idx => idx.IsUnique = true); (4)
            });

在这里,我们:

  1. 提供到Postgresql数据库的连接字符串。
  2. 自定义架构名称(如果不这样做,则将在公共架构中创建用于存储文档的表)。
  3. 自定义JsonSerializer,以便它可以序列化受保护的属性(这很重要,我们正在尝试根据DDD规则设计聚合,我们不想使用公共设置器公开内部状态)并处理对象之间的循环引用。
  4. 我们为保单号添加“重复”字段。 在这里,我们告诉Marten不仅将策略号存储为序列化JSON文档中聚合的一部分,而且还要创建单独的列和唯一索引以加快搜索速度。 这样做是因为我们想快速找到给定保单号的帐户。

有关架构和映射自定义的更多详细信息将在本文后面提供。

让我们看一下IDataStore接口的实现:

public class MartenDataStore : IDataStore
{
    private readonly IDocumentSession session;

    public MartenDataStore(IDocumentStore documentStore)
    {
        session = documentStore.LightweightSession();
        PolicyAccounts = new MartenPolicyAccountRepository(session);
    }

    public IPolicyAccountRepository PolicyAccounts { get; }

    public async Task CommitChanges()
    {
        await session.SaveChangesAsync();
    }

    ...
}

在构造函数中,我们打开文档会话。 当我们将丢弃类的实例时,我们将关闭它(此处省略IDisposable实现,但是您可以在GitHub上检查完整的代码)。 CommitChanges方法使用DocumentSession类的SaveChangesAsync方法。 在这里,我们使用Marten的异步API,但是如果您愿意,也可以使用同步版本。

IPolicyAccountRepository的实现非常简单。

public class MartenPolicyAccountRepository : IPolicyAccountRepository
{
    private readonly IDocumentSession documentSession;

    public MartenPolicyAccountRepository(IDocumentSession documentSession)
    {
        this.documentSession = documentSession;
    }

    public void Add(PolicyAccount policyAccount)
    {
        this.documentSession.Insert(policyAccount);
    }

    public async Task FindByNumber(string accountNumber)
    {
        return await this.documentSession
            .Query()
            .FirstOrDefaultAsync(p => p.PolicyNumber == accountNumber);
    }
}

我们接受对构造函数中打开文档会话的引用。 Add方法使用DocumentSessionInsert方法将文档注册为新的工作单元。在DataStore上调用CommitChanges时,文档将保存在数据库中。 CommitChanges在基础文档会话上调用SaveChanges

FindByNumber更加有趣,因为它表明您可以使用LINQ来构造对数据库中存储的文档的查询。在我们的例子中,这是一个非常简单的查询,它查找具有给定编号的策略帐户。我们将在本文中进一步详细描述Marten LINQ功能。

自定义架构和映射

Marten将为要保存到数据库的聚合的每种.NET类型创建一个表。 Marten还将为每个表生成一个"upsert"数据库函数。

默认情况下,表是在公共架构中创建的,并以"mt_doc_"前缀和您的类名的串联命名。
在我们的例子中,我们有PolicyAccount类,因此Marten创建了mt_doc_policyaccount表。

您可以使用各种自定义选项。

您可以指定要使用的数据库架构。在我们的案例中,我们希望将所有表创建为"payment_service"模式的一部分。

var store = DocumentStore.For(_ =>
{
    _.DatabaseSchemaName = "payment_service";
}

You can also specify schema for each table.

_.Storage.MappingFor(typeof(BillingPeriod)).DatabaseSchemaName = "billing";

默认情况下,Marten创建一个带有ID列的表,数据列序列为json,并添加一些元数据列:上次修改日期,.NET类型名称,版本(用于乐观并发)和软删除标记列。

Marten要求您的类具有将被映射到主键的属性。 默认情况下,Marten将查找名为:id,Id或ID的属性。 您可以通过使用[Identity]属性注释类的一个属性来更改它,或者在文档存储初始化代码中自定义映射。

var store = DocumentStore.For(_ =>
{
    _.Schema.For.Identity(x => x.MyId);
}

您还可以自定义ID生成策略。 例如,您可以选择使用CombGuid(顺序guid)。

_.Schema.For().IdStrategy(new CombGuidIdGeneration());

如果要提高查询性能,可以命令Marten创建索引和重复字段。
您的第一个选择是使用计算索引。 在下面的示例中,我们在所有者的名字和姓氏上创建了索引,因此按这两个字段进行搜索应该更快。

_.Schema.For().Index(x => x.Owner.FirstName);
_.Schema.For().Index(x => x.Owner.LastName);

请注意,计算索引不适用于DateTime和DateTimeOffset字段。

第二种选择是引入所谓的重复字段。 我们使用这种方法通过相应的保单号优化查找帐户。

_.Schema.For().Duplicate(t => t.PolicyNumber,pgType: "varchar(50)", configure: idx => idx.IsUnique = true);

在这里,我们告诉Marten为具有唯一索引的varchar(50)类型的策略号添加附加字段。 这样,Marten不仅将策略号保存为JSON数据的一部分,而且还将其保存到具有唯一索引的单独的列中,因此对它的搜索应该超级快。

您可以为给定类型启用乐观并发。

_.Schema.For().UseOptimisticConcurrency(true);

还有许多其他选项,例如全文本索引,外键可让我们链接两个聚合(gin / gist索引)。

保存聚合

借助IDataStoreIPolicyAccountRepository,可以轻松存储PolicyAccount聚合。
这是创建新帐户并将其保存在数据库中的示例代码。

public async Task Handle(PolicyCreated notification, CancellationToken cancellationToken)
{
            var policy = new PolicyAccount(notification.PolicyNumber, policyAccountNumberGenerator.Generate());

            using (dataStore)
            {
                dataStore.PolicyAccounts.Add(policy);
                await dataStore.CommitChanges();
            }
}

如您所见,保存和加载域对象不需要任何形式(代码或属性)或配置的映射。

您的类必须满足的唯一要求是:您的类必须可序列化为JSON(可以对JSON Serializer进行tweek配置,以使您的类正确地进行序列化/反序列化),您的类必须公开标识符字段或属性。标识符属性将用作主键的值源。字段/属性名称必须是ID或ID或ID,但是您可以使用[Identity]属性覆盖此规则,也可以在代码中自定义映射。以下数据类型可用作标识符:字符串GuidCombGuid(顺序GUID),intlong或自定义类。对于intlong而言,Marten使用HiLo生成器。 Marten确保在IDocumentSession.Store期间设置了标识符。

Marten还支持乐观并发。可以根据每种文档类型激活此功能。为了为您的类启用乐观并发,您可以将[UseOptimisticConcurrency]属性添加到您的类或自定义架构配置。

加载聚合

加载聚合也是微不足道的。

public async Task Handle(GetAccountBalanceQuery request, CancellationToken cancellationToken)
{
    var policyAccount = await dataStore.PolicyAccounts.FindByNumber(request.PolicyNumber);

    if (policyAccount == null)
    {
        throw new PolicyAccountNotFound(request.PolicyNumber);
    }

    return new GetAccountBalanceQueryResult
    {
        Balance = new PolicyAccountBalanceDto
        {
            PolicyNumber = policyAccount.PolicyNumber,
            PolicyAccountNumber = policyAccount.PolicyAccountNumber,
            Balance = policyAccount.BalanceAt(DateTimeOffset.Now)
        }
    };
} 

查询方式

Marten提供广泛的LINQ支持。 示例简单查询,查找具有给定编号的策略的策略帐户:

session.Query().Where(p => p.PolicyNumber == "12121212")

结合了多个条件和逻辑运算符的查询示例:

session.Query().Where(p => p.PolicyNumber == "12121212" && p.PolicyAccountNumber!="32323232323")

搜索您的汇总的子集合并查找条目金额等于200的帐户的示例:

var accounts  = session.Query()
                    .Where(p => p.Entries.Any(_ => _.Amount == 200.0M)).ToList()

请注意,搜索子集合的支持仅限于检查子集合的成员是否相等(但是在以后的Marten版本中可能会改变)。

您还可以在对象层次结构中进行深入搜索。 例如,如果我们将帐户所有者数据存储在保单帐户中,则可以像这样搜索给定人员的保单帐户:

var accounts  = session.Query()
                    .Where(p => p.Owner.Name.LastName == “Jones” && p.Owner.Name.FirstName == “Tim”)).ToList()

您可以使用StartsWithEndsWithContains搜索字符串字段。

session.Query().Where(p => p.PolicyNumber.EndsWith("009898"))

您可以根据聚合属性计算CountMinMaxAverageSum

session.Query().Max(p => p.PolicyAccountNumber)

您可以订购结果,并使用Take / Skip进行分页。

session.Query().Skip(10).Take(10).OrderBy(p => p.PolicyAccountNumber)

还有一个方便的快捷方式ToPagedList结合了跳过和采用。

如果您无法弄清楚为什么查询没有达到预期效果,Marten可以为您提供预览LINQ查询的功能。

var query = session.Query()
                    .Where(p => p.PolicyNumber == "1223");

                var cmd = query.ToCommand(FetchType.FetchMany);
                var sql = cmd.CommandText;

下面的代码片段将LINQ查询转换为ADO.NET命令,以便您可以检查查询文本和参数值。

可以在此处找到受支持的运算符的完整列表。

除了LINQ,您还可以使用SQL查询文档。

var user =
    session.Query("select data from payment_service.mt_doc_policyaccount where data ->> 'PolicyAccountNumber' = 1221212")
           .Single();

您可以选择从数据库检索原始JSON。

var json = session.Json.FindById(id);

编译查询

也有称为编译查询的高级功能。 LINQ在构造查询时非常酷并且有用,但是它具有一定的性能和内存使用开销。

如果您的查询很复杂并且经常执行,则可以利用编译查询。 使用编译的查询,您可以避免在每次查询执行时解析LINQ表达式树的开销。

编译查询是实现ICompiledQuery接口的类。
示例查询类,用于搜索具有给定编号的策略帐户。

public class FindAccountByNumberQuery : ICompiledQuery
{
    public string AccountNumber { get; set; }

    public Expression> QueryIs()
    {
        return q => q.FirstOrDefault(p => p.PolicyAccountNumber == AccountNumber);
    }
}

此处的关键方法是QueryIs。 此方法返回定义查询的表达式。

该类可以这样使用:

var account = session.Query(new FindAccountByNumberQuery {AccountNumber = "11121212"});

您可以在此处阅读有关编译查询的更多信息。

修补数据

Marten修补API可用于更新数据库中的现有文档。在某些情况下,这比将整个文档加载到内存,对其进行序列化,更改,反序列化然后保存回数据库更为有效。

修复数据错误和处理类结构中的更改时,修补程序API也非常有用。

我们的设计不会永远保持不变。随着时间的流逝,我们将向类中添加新属性,更改对集合的简单引用,或者相反。某些属性可能会被提取并重构为新类,某些属性可能会被丢弃。
使用关系数据库中的表时,我们有一组众所周知的SQL DDL命令,例如ALTER TABLE ADD / DROP COLUMN。

使用JSON文档时,我们必须以某种方式处理所有更改,以便在更改相应的类时仍可以加载和查询现有文档。

让我们尝试修改PolicyAccount类并迁移数据库中的现有数据,使其保持一致。
我们从PolicyAccount开始,必须具有代表帐户所有者姓氏和名字的属性。

public class PolicyAccount
{
    public Guid Id { get; protected set; }
    public string PolicyAccountNumber { get; protected set; }
    public string PolicyNumber { get; protected set; }
    public string OwnerFirstName { get; protected set; }
    public string OwnerName { get; protected set; }
    public ICollection Entries { get; protected set; }
    …

在数据库中,我们的数据如下所示:

{
    "Id": "51d43842-896d-4d92-b1b9-b4c6512d3cf7", 
    "$id": "2", 
    "Entries": [], 
    "OwnerName": "Jones", 
    "PolicyNumber": "POLICY_1", 
    "OwnerFirstName": "Tim", 
    "PolicyAccountNumber": "231232132131"
}

我们可以看到OwnerName不是最佳名称,我们想将其重命名为OwnerLastName

在C#上,这非常容易,因为大多数IDE都提供了开箱即用的重命名重构功能。 进行操作,然后使用Patch API修复数据库中的数据

public void RenameProperty()
{
    using (var session = SessionProvider.OpenSession())
    {
        session
            .Patch(x => x.OwnerLastName == null)
            .Rename("OwnerName", x => x.OwnerLastName);

        session.SaveChanges();
    }

}

如果运行此方法,数据库中的数据现在将如下所示:

{
    "Id": "51d43842-896d-4d92-b1b9-b4c6512d3cf7", 
    "$id": "2", 
    "Entries": [], 
    "PolicyNumber": "POLICY_1", 
    "OwnerLastName": "Jones", 
    "OwnerFirstName": "Tim", 
    "PolicyAccountNumber": "231232132131"
}

让我们尝试一些更复杂的事情。 我们决定将OwnerFirstNameOwnerLastName提取到一个类中。 现在,我们的C#代码如下所示:

public class PolicyAccount
{
        public Guid Id { get; protected set; }
        public string PolicyAccountNumber { get; protected set; }
        public string PolicyNumber { get; protected set; }
        public string OwnerFirstName { get; protected set; }
        public string OwnerLastName { get; protected set; }
        public Owner Owner { get; protected set; }
        public ICollection Entries { get; protected set; }
}

我们添加了一个具有FirstNameLastName属性的新类。 现在,我们将使用Patch API修复数据库中的数据。

public void AddANewProperty()
{
    using (var session = SessionProvider.OpenSession())
    {
         session
            .Patch(x=>x.Owner.LastName==null)
            .Duplicate(x => x.OwnerLastName, w => w.Owner.LastName);

        session
            .Patch(x=>x.Owner.FirstName==null)
            .Duplicate(x => x.OwnerFirstName, w => w.Owner.FirstName);

        session.SaveChanges();
    }
}

以及我们数据库中的数据:

{
    "Id": "51d43842-896d-4d92-b1b9-b4c6512d3cf7", 
    "$id": "2", 
    "Owner": {
        "LastName": "Jones", 
        "FirstName": "Tim"
    }, 
    "Entries": [], 
    "PolicyNumber": "POLICY_1", 
    "OwnerLastName": "Jones", 
    "OwnerFirstName": "Tim", 
    "PolicyAccountNumber": "231232132131"
}

现在是时候清理了。 我们必须从C#代码和数据库中的数据中删除未使用的OwnerFirstNameOwnerLastName属性。

public void RemoveProperty()
{
    using (var session = SessionProvider.OpenSession())
    {
        session
            .Patch(x=>x.Owner!=null)
            .Delete("OwnerLastName");

        session
            .Patch(x=>x.Owner!=null)
            .Delete("OwnerFirstName");

        session.SaveChanges();
    }
}

数据库中的数据现在看起来像这样。 OwnerFirstNameOwnerLastName不见了。

{
    "Id": "51d43842-896d-4d92-b1b9-b4c6512d3cf7", 
    "$id": "2", 
    "Owner": {
        "LastName": "Jones", 
        "FirstName": "Tim"
     }, 
    "Entries": [], 
    "PolicyNumber": "POLICY_1", 
    "PolicyAccountNumber": "231232132131"
}

补丁程序API提供了更多开箱即用的操作。你可以在这里读更多关于它的内容。
修补程序API要求您安装PostgreSQL PLV8引擎。

除了Marten的Patching API外,您始终可以使用PostgreSQL的全部功能,该功能可为您提供一组可与JSON类型一起使用的功能,并将其与使用JavaScript作为PLV8引擎提供的数据库功能/过程的语言相结合。实际上,Patch API生成的功能是用JavaScript编写的,并使用PLV8引擎在数据库中执行。

Marten利弊

优点

  • 两全其美:关系数据库的ACID和SQL支持以及文档数据库的易于使用和开发。
  • ACID支持使您可以在一个事务中保存来自许多不同表的许多文档,而大多数文档数据库都不支持。
  • 使用文档使您可以保存和加载文档,而不必定义使用关系数据库时在对象模型和数据库模型之间定义的映射。这样可以加快开发速度,尤其是在开发的早期阶段,您不必担心方案更改。
  • 对LINQ查询的广泛支持为EF和NHibernate用户提供了熟悉的体验。
  • 能够用作文档存储和事件存储。
  • 能够为您的单元/集成测试快速设置/拆卸数据。
  • 批量操作支持。
  • 用于修补现有文档的API。
  • 不能或无法执行LINQ查询时可以使用SQL。
  • 在集成测试中轻松使用真实的数据库。建立一个数据库,用初始数据填充它,然后清理它是非常简单和快速的。
  • 多租户支持。
  • 编译和批处理查询支持。
  • DocumentSession能够参与TransactionScope托管的事务。

缺点

  • 仅适用于PostgreSQL。
  • 使用Patch API进行数据迁移需要更多的工作和学习新知识。
  • 在子集合中搜索的支持有限。
  • 不太适合报表和临时查询。

总结

在为微服务设计数据访问策略时,有多个选项可供选择。除实体框架或手工SQL之外,还有其他选项。

Marten是一个成熟的库,具有许多有用的功能和良好的LINQ支持。如果您以PostgreSQL数据库为目标,并使用域驱动的设计方法将您的域模型划分为小的聚合,那么Marten值得一试。

它在设计和探索阶段或构建原型时也可能是非常有用的工具,使您可以快速演化域模型并能够持久化和查询数据。

你可能感兴趣的:([翻译] 基于.NET Core构建微服务 第五部分:Marten域聚合的理想仓库)