Dostawca Azure Cosmos DB dla rozwiązania EF Core

Ten dostawca baz danych umożliwia używanie rozwiązania Entity Framework Core z usługą Azure Cosmos DB. Dostawca jest utrzymywany w ramach projektu Entity Framework Core Project.

Zdecydowanie zalecamy zapoznanie się z dokumentacją usługi Azure Cosmos DB przed przeczytaniem tej sekcji.

Uwaga

Ten dostawca działa tylko z usługą Azure Cosmos DB for NoSQL.

Instalowanie

Zainstaluj pakiet NuGet Microsoft.EntityFrameworkCore.Cosmos.

dotnet add package Microsoft.EntityFrameworkCore.Cosmos

Rozpoczęcie pracy

Napiwek

Przykład z tego artykułu można zobaczyć w witrynie GitHub.

Podobnie jak w przypadku innych dostawców, pierwszym krokiem jest wywołanie metody UseCosmos:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.UseCosmos(
        "https://localhost:8081",
        "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
        databaseName: "OrdersDB");

Ostrzeżenie

Punkt końcowy i klucz są tutaj zakodowane na stałe, ale w aplikacji produkcyjnej powinny one być bezpiecznie przechowywane.

W tym przykładzie Order to prosta jednostka z odwołaniem do typu własnościowegoStreetAddress.

public class Order
{
    public int Id { get; set; }
    public int? TrackingNumber { get; set; }
    public string PartitionKey { get; set; }
    public StreetAddress ShippingAddress { get; set; }
}
public class StreetAddress
{
    public string Street { get; set; }
    public string City { get; set; }
}

Zapisywanie danych i uruchamianie zapytań odbywa się zgodnie z normalnym wzorcem rozwiązania EF:

using (var context = new OrderContext())
{
    await context.Database.EnsureDeletedAsync();
    await context.Database.EnsureCreatedAsync();

    context.Add(
        new Order
        {
            Id = 1, ShippingAddress = new StreetAddress { City = "London", Street = "221 B Baker St" }, PartitionKey = "1"
        });

    await context.SaveChangesAsync();
}

using (var context = new OrderContext())
{
    var order = await context.Orders.FirstAsync();
    Console.WriteLine($"First order will ship to: {order.ShippingAddress.Street}, {order.ShippingAddress.City}");
    Console.WriteLine();
}

Ważne

Wywołanie metody EnsureCreatedAsync jest konieczne, aby utworzyć wymagane kontenery i wstawić dane inicjujące, jeśli istnieją w modelu. Jednak metoda EnsureCreatedAsync powinna być wywoływana tylko podczas wdrażania, a nie podczas normalnego działania, ponieważ może to powodować problemy z wydajnością.

Opcje usługi Azure Cosmos DB

Istnieje również możliwość skonfigurowania dostawcy usługi Azure Cosmos DB przy użyciu jednego parametry połączenia i określenia innych opcji dostosowywania połączenia:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.UseCosmos(
        "AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
        databaseName: "OptionsDB",
        options =>
        {
            options.ConnectionMode(ConnectionMode.Gateway);
            options.WebProxy(new WebProxy());
            options.LimitToEndpoint();
            options.Region(Regions.AustraliaCentral);
            options.GatewayModeMaxConnectionLimit(32);
            options.MaxRequestsPerTcpConnection(8);
            options.MaxTcpConnectionsPerEndpoint(16);
            options.IdleTcpConnectionTimeout(TimeSpan.FromMinutes(1));
            options.OpenTcpConnectionTimeout(TimeSpan.FromMinutes(1));
            options.RequestTimeout(TimeSpan.FromMinutes(1));
        });

Napiwek

Zapoznaj się z dokumentacją opcji usługi Azure Cosmos DB, aby uzyskać szczegółowy opis efektów każdej z powyższych opcji.

Dostosowywanie modelu specyficznego dla usługi Cosmos

Domyślnie wszystkie typy jednostek są mapowane na ten sam kontener, nazwany zgodnie z kontekstem pochodnym ("OrderContext" w tym przypadku). Aby zmienić domyślną nazwę kontenera, użyj metody HasDefaultContainer:

modelBuilder.HasDefaultContainer("Store");

Aby zamapować typ jednostki na inny kontener, użyj metody ToContainer:

modelBuilder.Entity<Order>()
    .ToContainer("Orders");

Aby zidentyfikować typ jednostki, który reprezentuje dany element, rozwiązanie EF Core dodaje wartość rozróżniacza, nawet jeśli nie ma żadnych typów jednostek pochodnych. Nazwę i wartość rozróżniacza można zmienić.

Jeśli żaden inny typ jednostki nigdy nie będzie przechowywany w tym samym kontenerze, można usunąć rozróżniacz, wywołując metodę HasNoDiscriminator:

modelBuilder.Entity<Order>()
    .HasNoDiscriminator();

Klucze partycji

Domyślnie program EF Core utworzy kontenery z kluczem partycji ustawionym na "__partitionKey" wartość bez podawania żadnej wartości podczas wstawiania elementów. Jednak aby w pełni wykorzystać możliwości wydajności usługi Azure Cosmos DB, należy użyć starannie wybranego klucza partycji. Można go skonfigurować, wywołując metodę HasPartitionKey:

modelBuilder.Entity<Order>()
    .HasPartitionKey(o => o.PartitionKey);

Uwaga

Właściwość dotycząca klucza partycji może mieć dowolny typ, o ile można go przekonwertować na ciąg.

Po skonfigurowaniu właściwość dotycząca klucza partycji zawsze powinna mieć wartość inną niż null. Można określić, że zapytanie ma dotyczyć jednej partycji, dodając wywołanie metody WithPartitionKey.

using (var context = new OrderContext())
{
    context.Add(
        new Order
        {
            Id = 2, ShippingAddress = new StreetAddress { City = "New York", Street = "11 Wall Street" }, PartitionKey = "2"
        });

    await context.SaveChangesAsync();
}

using (var context = new OrderContext())
{
    var order = await context.Orders.WithPartitionKey("2").LastAsync();
    Console.WriteLine($"Last order will ship to: {order.ShippingAddress.Street}, {order.ShippingAddress.City}");
    Console.WriteLine();
}

Zazwyczaj zalecane jest dodanie klucza partycji do klucza podstawowego, ponieważ najlepiej odzwierciedla to semantykę serwera i umożliwia pewne optymalizacje, na przykład w metodzie FindAsync.

Aprowizowana przepływność

Jeśli używasz programu EF Core do tworzenia bazy danych lub kontenerów usługi Azure Cosmos DB, możesz skonfigurować aprowizowaną przepływność dla bazy danych przez wywołanie metody CosmosModelBuilderExtensions.HasAutoscaleThroughput lub CosmosModelBuilderExtensions.HasManualThroughput. Przykład:

modelBuilder.HasManualThroughput(2000);
modelBuilder.HasAutoscaleThroughput(4000);

Aby skonfigurować aprowizowaną przepływność dla kontenera, wywołaj metodę CosmosEntityTypeBuilderExtensions.HasAutoscaleThroughput lub CosmosEntityTypeBuilderExtensions.HasManualThroughput. Przykład:

modelBuilder.Entity<Family>(
    entityTypeBuilder =>
    {
        entityTypeBuilder.HasManualThroughput(5000);
        entityTypeBuilder.HasAutoscaleThroughput(3000);
    });

Jednostki osadzone

Uwaga

Powiązane typy jednostek są domyślnie konfigurowane jako należące do użytkownika. Aby temu zapobiec dla określonego typu jednostki, wywołaj metodę ModelBuilder.Entity.

W przypadku usługi Azure Cosmos DB jednostki należące do firmy są osadzone w tym samym elemencie co właściciel. Aby zmienić nazwę właściwości, użyj metody ToJsonProperty:

modelBuilder.Entity<Order>().OwnsOne(
    o => o.ShippingAddress,
    sa =>
    {
        sa.ToJsonProperty("Address");
        sa.Property(p => p.Street).ToJsonProperty("ShipsToStreet");
        sa.Property(p => p.City).ToJsonProperty("ShipsToCity");
    });

W przypadku tej konfiguracji zamówienie z powyższego przykładu jest zapisywane w następujący sposób:

{
    "Id": 1,
    "PartitionKey": "1",
    "TrackingNumber": null,
    "id": "1",
    "Address": {
        "ShipsToCity": "London",
        "ShipsToStreet": "221 B Baker St"
    },
    "_rid": "6QEKAM+BOOABAAAAAAAAAA==",
    "_self": "dbs/6QEKAA==/colls/6QEKAM+BOOA=/docs/6QEKAM+BOOABAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-683c-692e763901d5\"",
    "_attachments": "attachments/",
    "_ts": 1568163674
}

Kolekcje jednostek własnościowych również są osadzone. W następnym przykładzie użyjemy klasy Distributor z kolekcją obiektów StreetAddress:

public class Distributor
{
    public int Id { get; set; }
    public string ETag { get; set; }
    public ICollection<StreetAddress> ShippingCenters { get; set; }
}

Jednostki własnościowe nie muszą udostępniać jawnych wartości klucza na potrzeby przechowywania:

var distributor = new Distributor
{
    Id = 1,
    ShippingCenters = new HashSet<StreetAddress>
    {
        new StreetAddress { City = "Phoenix", Street = "500 S 48th Street" },
        new StreetAddress { City = "Anaheim", Street = "5650 Dolly Ave" }
    }
};

using (var context = new OrderContext())
{
    context.Add(distributor);

    await context.SaveChangesAsync();
}

Będą one utrwalane w ten sposób:

{
    "Id": 1,
    "Discriminator": "Distributor",
    "id": "Distributor|1",
    "ShippingCenters": [
        {
            "City": "Phoenix",
            "Street": "500 S 48th Street"
        },
        {
            "City": "Anaheim",
            "Street": "5650 Dolly Ave"
        }
    ],
    "_rid": "6QEKANzISj0BAAAAAAAAAA==",
    "_self": "dbs/6QEKAA==/colls/6QEKANzISj0=/docs/6QEKANzISj0BAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-683c-7b2b439701d5\"",
    "_attachments": "attachments/",
    "_ts": 1568163705
}

Wewnętrznie rozwiązanie EF Core zawsze musi mieć unikatowe wartości klucza dla wszystkich śledzonych jednostek. Klucz podstawowy tworzony domyślnie dla kolekcji typów własnościowych zawiera właściwość klucza obcego wskazującą właściciela oraz właściwość int odpowiadającą indeksowi w tablicy JSON. Aby pobrać te wartości, można użyć interfejsu API wpisów:

using (var context = new OrderContext())
{
    var firstDistributor = await context.Distributors.FirstAsync();
    Console.WriteLine($"Number of shipping centers: {firstDistributor.ShippingCenters.Count}");

    var addressEntry = context.Entry(firstDistributor.ShippingCenters.First());
    var addressPKProperties = addressEntry.Metadata.FindPrimaryKey().Properties;

    Console.WriteLine(
        $"First shipping center PK: ({addressEntry.Property(addressPKProperties[0].Name).CurrentValue}, {addressEntry.Property(addressPKProperties[1].Name).CurrentValue})");
    Console.WriteLine();
}

Napiwek

W razie potrzeby można zmienić domyślny klucz podstawowy dla własnościowych typów jednostek, ale wówczas konieczne jest jawne podawanie wartości kluczy.

Kolekcje typów pierwotnych

Kolekcje obsługiwanych typów pierwotnych, takich jak string i int, są odnajdywane i mapowane automatycznie. Obsługa kolekcji obejmuje wszystkie typy implementujące interfejs IReadOnlyList<T> lub IReadOnlyDictionary<TKey,TValue>. Rozważmy na przykład ten typ jednostki:

public class Book
{
    public Guid Id { get; set; }
    public string Title { get; set; }
    public IList<string> Quotes { get; set; }
    public IDictionary<string, string> Notes { get; set; }
}

Zarówno listę, jak i słownik można wypełnić i wstawić w bazie danych w normalny sposób:

using var context = new BooksContext();

var book = new Book
{
    Title = "How It Works: Incredible History",
    Quotes = new List<string>
    {
        "Thomas (Tommy) Flowers was the British engineer behind the design of the Colossus computer.",
        "Invented originally for Guinness, plastic widgets are nitrogen-filled spheres.",
        "For 20 years after its introduction in 1979, the Walkman dominated the personal stereo market."
    },
    Notes = new Dictionary<string, string>
    {
        { "121", "Fridges" },
        { "144", "Peter Higgs" },
        { "48", "Saint Mark's Basilica" },
        { "36", "The Terracotta Army" }
    }
};

context.Add(book);
context.SaveChanges();

Spowoduje to utworzenie następującego dokumentu JSON:

{
    "Id": "0b32283e-22a8-4103-bb4f-6052604868bd",
    "Discriminator": "Book",
    "Notes": {
        "36": "The Terracotta Army",
        "48": "Saint Mark's Basilica",
        "121": "Fridges",
        "144": "Peter Higgs"
    },
    "Quotes": [
        "Thomas (Tommy) Flowers was the British engineer behind the design of the Colossus computer.",
        "Invented originally for Guinness, plastic widgets are nitrogen-filled spheres.",
        "For 20 years after its introduction in 1979, the Walkman dominated the personal stereo market."
    ],
    "Title": "How It Works: Incredible History",
    "id": "Book|0b32283e-22a8-4103-bb4f-6052604868bd",
    "_rid": "t-E3AIxaencBAAAAAAAAAA==",
    "_self": "dbs/t-E3AA==/colls/t-E3AIxaenc=/docs/t-E3AIxaencBAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-9b50-fc769dc901d7\"",
    "_attachments": "attachments/",
    "_ts": 1630075016
}

Te kolekcje można następnie aktualizować, również w normalny sposób:

book.Quotes.Add("Pressing the emergency button lowered the rods again.");
book.Notes["48"] = "Chiesa d'Oro";

context.SaveChanges();

Ograniczenia:

  • Obsługiwane są tylko słowniki z kluczami ciągów
  • Uruchamianie zapytań dotyczących zawartości kolekcji pierwotnych nie jest obecnie obsługiwane. Zagłosuj na problemy #16926, #25700 i #25701, jeśli te funkcje są dla Ciebie ważne.

Praca z odłączonymi jednostkami

Każdy element musi mieć wartość id unikatową dla danego klucza partycji. Domyślnie rozwiązanie EF Core generuje wartość, łącząc wartości rozróżniacza i klucza podstawowego przy użyciu znaku „|” jako ogranicznika. Wartości klucza są generowane tylko wtedy, gdy jednostka przechodzi w stan Added. Może to stanowić problem podczas dołączania jednostek, jeśli nie mają one właściwości id w typie platformy .NET do przechowywania wartości.

W celu obejścia tego ograniczenia można utworzyć i ustawić wartość id ręcznie lub oznaczyć jednostkę jako dodaną, a następnie zmienić ją tak, aby miała żądany stan:

using (var context = new OrderContext())
{
    var distributorEntry = context.Add(distributor);
    distributorEntry.State = EntityState.Unchanged;

    distributor.ShippingCenters.Remove(distributor.ShippingCenters.Last());

    await context.SaveChangesAsync();
}

using (var context = new OrderContext())
{
    var firstDistributor = await context.Distributors.FirstAsync();
    Console.WriteLine($"Number of shipping centers is now: {firstDistributor.ShippingCenters.Count}");

    var distributorEntry = context.Entry(firstDistributor);
    var idProperty = distributorEntry.Property<string>("__id");
    Console.WriteLine($"The distributor 'id' is: {idProperty.CurrentValue}");
}

Oto wynikowy kod JSON:

{
    "Id": 1,
    "Discriminator": "Distributor",
    "id": "Distributor|1",
    "ShippingCenters": [
        {
            "City": "Phoenix",
            "Street": "500 S 48th Street"
        }
    ],
    "_rid": "JBwtAN8oNYEBAAAAAAAAAA==",
    "_self": "dbs/JBwtAA==/colls/JBwtAN8oNYE=/docs/JBwtAN8oNYEBAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-9377-d7a1ae7c01d5\"",
    "_attachments": "attachments/",
    "_ts": 1572917100
}

Optymistyczna współbieżność przy użyciu elementów eTag

Aby skonfigurować typ jednostki w celu używania optymistycznej współbieżności, wywołaj metodę UseETagConcurrency. To wywołanie spowoduje utworzenie właściwości _etag w stanie towarzyszącym i ustawienie jej jako tokenu współbieżności.

modelBuilder.Entity<Order>()
    .UseETagConcurrency();

Aby ułatwić usuwanie błędów współbieżności, możesz zamapować element eTag na właściwość CLR przy użyciu metody IsETagConcurrency.

modelBuilder.Entity<Distributor>()
    .Property(d => d.ETag)
    .IsETagConcurrency();