Udostępnij przez


Zmiany powodujące niezgodność w programie EF Core 10 (EF10)

Ta strona dokumentuje zmiany interfejsu API i zachowania, które mogą uszkodzić istniejące aplikacje aktualizowane z programu EF Core 9 do EF Core 10. Pamiętaj, aby przejrzeć wcześniejsze zmiany powodujące niezgodność w przypadku aktualizacji z wcześniejszej wersji programu EF Core:

Streszczenie

Uwaga / Notatka

Jeśli używasz biblioteki Microsoft.Data.Sqlite, zapoznaj się z poniższą sekcją dotyczącą zmian w Microsoft.Data.Sqlite powodujących niezgodność.

zmiana łamiąca zgodność Wpływ
Narzędzia EF wymagają teraz określenia struktury dla projektów wielokierunkowych Średni
Nazwa aplikacji jest teraz dodawana do ciągu połączenia Niski
Typ danych JSON programu SQL Server używany domyślnie w usłudze Azure SQL i poziom zgodności 170 Niski
Kolekcje sparametryzowane domyślnie używają wielu parametrów Niski
ExecuteUpdateAsync akceptuje teraz regularną, a nie wyrażeniową lambdę Niski
Nazwy kolumn złożonych typów uzyskują teraz unikalne nazwy Niski
Zagnieżdżone właściwości typu złożonego używają pełnej ścieżki w nazwach kolumn Niski
Zmieniono podpis IDiscriminatorPropertySetConvention Niski
Metody IRelationalCommandDiagnosticsLogger dodaj parametr logCommandText Niski

Zmiany o średnim wpływie

Narzędzia EF wymagają teraz określenia struktury dla projektów wielokierunkowych

Problem ze śledzeniem nr 37230

Stare zachowanie

Wcześniej narzędzia EF (dotnet-ef) mogły być używane w projektach przeznaczonych dla wielu platform bez określania struktury do użycia.

Nowe zachowanie

Począwszy od programu EF Core 10.0, podczas uruchamiania narzędzi EF w projekcie przeznaczonym dla wielu platform (przy użyciu <TargetFrameworks> zamiast <TargetFramework>), należy jawnie określić platformę docelową do użycia z opcją --framework . Bez tej opcji zostanie zgłoszony następujący błąd:

Projekt jest przeznaczony dla wielu frameworków. Użyj opcji --framework, aby określić platformę docelową do użycia.

Dlaczego

W programie EF Core 10 narzędzia zaczęły polegać na ResolvePackageAssets zadaniu MSBuild, aby uzyskać dokładniejsze informacje o zależnościach projektu. Jednak to zadanie nie jest dostępne, jeśli projekt jest przeznaczony dla wielu platform docelowych (TFM). Rozwiązanie wymaga od użytkowników wybrania platformy, która ma być używana.

Środki zaradcze

W przypadku uruchamiania dowolnego polecenia narzędzi EF w projekcie, który jest przeznaczony dla wielu platform, określ platformę docelową --framework przy użyciu opcji . Przykład:

dotnet ef migrations add MyMigration --framework net9.0
dotnet ef database update --framework net9.0
dotnet ef migrations script --framework net9.0

Jeśli plik projektu wygląda następująco:

<PropertyGroup>
  <TargetFrameworks>net8.0;net9.0</TargetFrameworks>
</PropertyGroup>

Podczas uruchamiania narzędzi EF należy wybrać jedną ze struktur (np net9.0. ).

Zmiany o niskim wpływie

Nazwa aplikacji jest teraz wstrzykiwana do łańcucha połączenia

Problem ze śledzeniem nr 35730

Nowe zachowanie

Gdy ciąg połączenia bez elementu Application Name jest przekazywany do EF, EF teraz wstawia Application Name zawierający anonimowe informacje o używanych wersjach EF i SqlClient. W zdecydowanej większości przypadków nie ma to wpływu na aplikację w żaden sposób, ale może mieć wpływ na zachowanie w niektórych przypadkach brzegowych. Jeśli na przykład połączysz się z tą samą bazą danych za pomocą programu EF i innej technologii dostępu do danych innych niż EF (np. Dapper, ADO.NET), program SqlClient użyje innej wewnętrznej puli połączeń, ponieważ program EF będzie teraz używać innych, zaktualizowanych parametrów połączenia (jeden, w którym Application Name zostało wprowadzone). Jeśli ten rodzaj dostępu mieszanego jest wykonywany w ramach TransactionScope, może to spowodować eskalację do transakcji rozproszonej, która wcześniej nie była konieczna, z powodu użycia dwóch ciągów połączenia, które SqlClient identyfikuje jako dwie odrębne bazy danych.

Środki zaradcze

Środek zaradczy polega na zdefiniowaniu Application Name w parametrach połączenia. Po zdefiniowaniu jednego, program EF nie zastępuje tego, a oryginalny ciąg połączenia jest zachowywany dokładnie w pierwotnej formie.

Typ danych JSON programu SQL Server używany domyślnie w usłudze Azure SQL i poziom zgodności 170

Problem ze śledzeniem nr 36372

Stare zachowanie

Wcześniej podczas mapowania kolekcji pierwotnych lub typów należących do formatu JSON w bazie danych dostawca programu SQL Server przechowywał dane JSON w nvarchar(max) kolumnie:

public class Blog
{
    // ...

    // Primitive collection, mapped to nvarchar(max) JSON column
    public string[] Tags { get; set; }
    // Owned entity type mapped to nvarchar(max) JSON column
    public List<Post> Posts { get; set; }
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>().OwnsMany(b => b.Posts, b => b.ToJson());
}

W przypadku powyższych danych program EF wygenerował wcześniej następującą tabelę:

CREATE TABLE [Blogs] (
    ...
    [Tags] nvarchar(max),
    [Posts] nvarchar(max)
);

Nowe zachowanie

W przypadku programu EF 10, jeśli skonfigurujesz program EF UseAzureSql (zobacz dokumentację) lub skonfigurujesz program EF z poziomem zgodności 170 lub nowszym (zobacz dokumentację), program EF zamapuje zamiast tego na nowy typ danych JSON:

CREATE TABLE [Blogs] (
    ...
    [Tags] json
    [Posts] json
);

Mimo że nowy typ danych JSON jest zalecanym sposobem przechowywania danych JSON w programie SQL Server w przyszłości, mogą wystąpić pewne różnice behawioralne podczas przechodzenia z nvarchar(max)systemu , a niektóre określone formularze zapytań mogą nie być obsługiwane. Na przykład program SQL Server nie obsługuje operatora DISTINCT w tablicach JSON, a zapytania próbujące to zrobić nie powiedzie się.

Należy pamiętać, że jeśli masz istniejącą tabelę i używasz UseAzureSqlprogramu , uaktualnienie do programu EF 10 spowoduje wygenerowanie migracji, co spowoduje zmianę wszystkich istniejących nvarchar(max) kolumn JSON na json. Ta operacja zmiany jest obsługiwana i powinna być stosowana bezproblemowo i bez żadnych problemów, ale jest to nietrywialna zmiana bazy danych.

Dlaczego

Nowy typ danych JSON wprowadzony przez program SQL Server to doskonały, 1-klasowy sposób przechowywania i interakcji z danymi JSON w bazie danych; w szczególności przynosi znaczne ulepszenia wydajności (zobacz dokumentację). Wszystkie aplikacje korzystające z usługi Azure SQL Database lub SQL Server 2025 są zachęcane do migracji do nowego typu danych JSON.

Środki zaradcze

Jeśli używasz usługi Azure SQL Database i nie chcesz od razu przechodzić do nowego typu danych JSON, możesz skonfigurować program EF z poziomem zgodności niższym niż 170:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.UseAzureSql("<connection string>", o => o.UseCompatibilityLevel(160));
}

Jeśli używasz lokalnego programu SQL Server, domyślny poziom zgodności z UseSqlServer programem wynosi obecnie 150 (SQL Server 2019), więc typ danych JSON nie jest używany.

Alternatywnie można jawnie ustawić typ kolumny dla określonych właściwości na wartość nvarchar(max):

public class Blog
{
    public string[] Tags { get; set; }
    public List<Post> Posts { get; set; }
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>().PrimitiveCollection(b => b.Tags).HasColumnType("nvarchar(max)");
    modelBuilder.Entity<Blog>().OwnsMany(b => b.Posts, b => b.ToJson().HasColumnType("nvarchar(max)"));
    modelBuilder.Entity<Blog>().ComplexProperty(e => e.Posts, b => b.ToJson());
}

Kolekcje sparametryzowane domyślnie używają wielu parametrów

Problem ze śledzeniem nr 34346

Stare zachowanie

W EF Core 9 i wcześniejszych wersjach, sparametryzowane kolekcje w zapytaniach LINQ (takie jak używane z .Contains()) były domyślnie tłumaczone na SQL za pomocą parametru tablicy JSON. Rozważ następujące zapytanie:

int[] ids = [1, 2, 3];
var blogs = await context.Blogs.Where(b => ids.Contains(b.Id)).ToListAsync();

W programie SQL Server wygenerowano następujący kod SQL:

@__ids_0='[1,2,3]'

SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Id] IN (
    SELECT [i].[value]
    FROM OPENJSON(@__ids_0) WITH ([value] int '$') AS [i]
)

Nowe zachowanie

Począwszy od programu EF Core 10.0, kolekcje sparametryzowane są teraz tłumaczone przy użyciu wielu parametrów skalarnych domyślnie:

SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Id] IN (@ids1, @ids2, @ids3)

Dlaczego

Nowe domyślne tłumaczenie dostarcza programowi optymalizacji zapytań informacje o kardynalności kolekcji, co może prowadzić do lepszych planów zapytań w wielu scenariuszach. Podejście wieloparametrowe równoważy efektywność pamięci podręcznej planu (przez parametryzowanie) i optymalizację zapytań (poprzez dostarczanie kardynalności).

Jednak różnorodne obciążenia mogą czerpać korzyści z różnych strategii translacji, w zależności od rozmiarów kolekcji, wzorców zapytań i charakterystyki bazy danych.

Środki zaradcze

Jeśli wystąpią problemy z nowym zachowaniem domyślnym (takim jak regresje wydajności), możesz skonfigurować tryb tłumaczenia globalnie:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseSqlServer("<CONNECTION STRING>", 
            o => o.UseParameterizedCollectionMode(ParameterTranslationMode.Constant));

Dostępne tryby to:

  • ParameterTranslationMode.MultipleParameters - Nowa wartość domyślna (wiele parametrów skalarnych)
  • ParameterTranslationMode.Constant - Wartości linii jako stałe (zachowanie domyślne pre-EF8)
  • ParameterTranslationMode.Parameter — Używa parametru tablicy JSON (domyślne ef8-9)

Tłumaczenie można również kontrolować na podstawie poszczególnych zapytań:

// Use constants instead of parameters for this specific query
var blogs = await context.Blogs
    .Where(b => EF.Constant(ids).Contains(b.Id))
    .ToListAsync();

// Use a single parameter (e.g. JSON parameter with OPENJSON) instead of parameters for this specific query
var blogs = await context.Blogs
    .Where(b => EF.Parameter(ids).Contains(b.Id))
    .ToListAsync();

// Use multiple scalar parameters for this specific query. This is the default in EF 10, but is useful if the default was changed globally:
var blogs = await context.Blogs
    .Where(b => EF.MultipleParameters(ids).Contains(b.Id))
    .ToListAsync();

Aby uzyskać więcej informacji na temat sparametryzowanego tłumaczenia kolekcji, zobacz dokumentację.

ExecuteUpdateAsync akceptuje teraz zwykłą lambda bez wyrażeń

Śledzenie problemu #32018

Stare zachowanie

Wcześniej ExecuteUpdate zaakceptował argument drzewa wyrażeń (Expression<Func<...>>) dla ustawiaczy kolumn.

Nowe zachowanie

Począwszy od EF Core 10.0, ExecuteUpdate akceptuje teraz argument bez wyrażeń (Func<...>) dla ustawień kolumn. Jeśli tworzysz drzewa wyrażeń w celu dynamicznego tworzenia argumentu ustawiających kolumny, kod nie będzie już kompilowany — ale może zostać zastąpiony znacznie prostszą alternatywą (zobacz poniżej).

Dlaczego

Fakt, że parametr ustawiaczy kolumn był drzewem wyrażeń, utrudniał dynamiczną konstrukcję ustawiaczy kolumn, gdzie niektórzy ustawiacze są obecni tylko w oparciu o jakiś warunek (zobacz Sposoby obejścia poniżej jako przykład).

Środki zaradcze

Kod tworzący drzewa wyrażeń w celu dynamicznego utworzenia argumentu setterów kolumn będzie musiał zostać przepisany — ale wynik będzie znacznie prostszy. Załóżmy na przykład, że chcemy zaktualizować widoki bloga, ale warunkowo także jego nazwę. Ponieważ argument setters był drzewem wyrażeń, należało napisać kod, taki jak poniższy.

// Base setters - update the Views only
Expression<Func<SetPropertyCalls<Blog>, SetPropertyCalls<Blog>>> setters =
    s => s.SetProperty(b => b.Views, 8);

// Conditionally add SetProperty(b => b.Name, "foo") to setters, based on the value of nameChanged
if (nameChanged)
{
    var blogParameter = Expression.Parameter(typeof(Blog), "b");

    setters = Expression.Lambda<Func<SetPropertyCalls<Blog>, SetPropertyCalls<Blog>>>(
        Expression.Call(
            instance: setters.Body,
            methodName: nameof(SetPropertyCalls<Blog>.SetProperty),
            typeArguments: [typeof(string)],
            arguments:
            [
                Expression.Lambda<Func<Blog, string>>(Expression.Property(blogParameter, nameof(Blog.Name)), blogParameter),
                Expression.Constant("foo")
            ]),
        setters.Parameters);
}

await context.Blogs.ExecuteUpdateAsync(setters);

Ręczne tworzenie drzew wyrażeń jest skomplikowane i podatne na błędy i sprawiło, że ten typowy scenariusz był znacznie trudniejszy niż powinien być. Począwszy od programu EF 10, możesz teraz napisać następujące zamiast tego:

await context.Blogs.ExecuteUpdateAsync(s =>
{
    s.SetProperty(b => b.Views, 8);
    if (nameChanged)
    {
        s.SetProperty(b => b.Name, "foo");
    }
});

Nazwy kolumn typu złożonego są teraz niekwyfikowane

Problem ze śledzeniem nr 4970

Stare zachowanie

Wcześniej podczas mapowania złożonych typów na kolumny tabeli, jeśli wiele właściwości w różnych typach złożonych ma tę samą nazwę kolumny, będą one w trybie dyskretnym współużytkować tę samą kolumnę.

Nowe zachowanie

Począwszy od EF Core 10.0, nazwy kolumn typu złożonego są zunikalizowane przez dołączenie liczby na końcu, jeśli istnieje w tabeli inna kolumna o tej samej nazwie.

Dlaczego

Zapobiega to uszkodzeniu danych, które mogą wystąpić, gdy wiele właściwości jest przypadkowo mapowanych na tę samą kolumnę.

Środki zaradcze

Jeśli potrzebujesz wielu właściwości do współużytkowania tej samej kolumny, skonfiguruj je jawnie przy użyciu metod Property i HasColumnName:

modelBuilder.Entity<Customer>(b =>
{
    b.ComplexProperty(c => c.ShippingAddress, p => p.Property(a => a.Street).HasColumnName("Street"));
    b.ComplexProperty(c => c.BillingAddress, p => p.Property(a => a.Street).HasColumnName("Street"));
});

Zagnieżdżone właściwości typu złożonego używają pełnej ścieżki w nazwach kolumn

Stare zachowanie

Wcześniej właściwości w złożonych, zagnieżdżonych typach były mapowane na kolumny przy użyciu tylko nazwy typu deklarującego. Na przykład EntityType.Complex.NestedComplex.Property został zamapowany na kolumnę NestedComplex_Property.

Nowe zachowanie

Począwszy od programu EF Core 10.0, właściwości zagnieżdżonych typów złożonych używają pełnej ścieżki do właściwości w ramach nazwy kolumny. Na przykład EntityType.Complex.NestedComplex.Property jest teraz mapowany na kolumnę Complex_NestedComplex_Property.

Dlaczego

Zapewnia to lepszą unikatowość nazw kolumn i ułatwia określenie, które właściwości są mapowane na które kolumny.

Środki zaradcze

Jeśli musisz zachować stare nazwy kolumn, skonfiguruj je jawnie przy użyciu elementów Property i HasColumnName:

modelBuilder.Entity<EntityType>()
    .ComplexProperty(e => e.Complex)
    .ComplexProperty(o => o.NestedComplex)
    .Property(c => c.Property)
    .HasColumnName("NestedComplex_Property");

Zmieniono podpis IDiscriminatorPropertySetConvention

Stare zachowanie

Wcześniej IDiscriminatorPropertySetConvention.ProcessDiscriminatorPropertySet przyjmował IConventionEntityTypeBuilder jako parametr.

Nowe zachowanie

Począwszy od programu EF Core 10.0, sygnatura metody została zmieniona na wartość IConventionTypeBaseBuilder zamiast IConventionEntityTypeBuilder.

Dlaczego

Ta zmiana umożliwia konwencję pracy zarówno z typami jednostek, jak i typami złożonymi.

Środki zaradcze

Zaktualizuj swoje implementacje niestandardowych konwencji, aby używać nowego podpisu.

public virtual void ProcessDiscriminatorPropertySet(
    IConventionTypeBaseBuilder typeBaseBuilder, // Changed from IConventionEntityTypeBuilder
    string name,
    Type type,
    MemberInfo memberInfo,
    IConventionContext<IConventionProperty> context)

Metody IRelationalCommandDiagnosticsLogger dodaj parametr logCommandText

Problem ze śledzeniem nr 35757

Stare zachowanie

Wcześniej metody na IRelationalCommandDiagnosticsLogger, takie jak CommandReaderExecuting, CommandReaderExecuted, CommandScalarExecuting, i inne, przyjmowały parametr command reprezentujący wykonywane polecenie bazy danych.

Nowe zachowanie

Począwszy od programu EF Core 10.0, te metody wymagają teraz dodatkowego logCommandText parametru. Parametr zawiera tekst polecenia SQL, który zostanie zarejestrowany i może mieć zredagowane dane poufne, gdy EnableSensitiveDataLogging nie jest włączone.

Dlaczego

Ta zmiana obsługuje nową funkcję domyślnego usuwania wbudowanych stałych z logów. Gdy EF wstawia wartości parametrów do zapytania SQL (np. w przypadku używania EF.Constant()), te wartości są teraz usuwane z dzienników, chyba że rejestrowanie poufnych danych zostanie wyraźnie włączone. Parametr logCommandText udostępnia zredagowany kod SQL do celów rejestrowania, podczas gdy command parametr zawiera rzeczywisty kod SQL, który jest wykonywany.

Środki zaradcze

Jeśli masz niestandardową implementację IRelationalCommandDiagnosticsLogger, musisz zaktualizować sygnatury metod, aby uwzględnić nowy parametr logCommandText. Przykład:

public InterceptionResult<DbDataReader> CommandReaderExecuting(
    IRelationalConnection connection,
    DbCommand command,
    DbContext context,
    Guid commandId,
    Guid connectionId,
    DateTimeOffset startTime,
    string logCommandText) // New parameter
{
    // Use logCommandText for logging purposes
    // Use command for execution-related logic
}

Parametr logCommandText zawiera kod SQL do zarejestrowania (ze wbudowanymi stałymi potencjalnie wyredagowanymi), a command.CommandText zawiera rzeczywisty kod SQL, który ma zostać wykonany na bazie danych.

Microsoft.Data.Sqlite — zmiany powodujące niezgodność

Streszczenie

zmiana łamiąca zgodność Wpływ
Korzystanie z funkcji GetDateTimeOffset bez przesunięcia teraz przyjmuje, że to UTC Wysoki
Zapisywanie wartości DateTimeOffset w kolumnie REAL jest teraz zapisywane w formacie UTC Wysoki
Użycie funkcji GetDateTime z przesunięciem zwraca teraz wartość w formacie UTC Wysoki

Zmiany o dużym wpływie

Korzystanie z funkcji GetDateTimeOffset bez przesunięcia zakłada teraz UTC

Zagadnienie śledzenia nr 36195

Stare zachowanie

Wcześniej, w przypadku używania GetDateTimeOffset na tekstowym znaczniku czasu, który nie miał przesunięcia (np. 2014-04-15 10:47:16), Microsoft.Data.Sqlite zakładał, że wartość znajdowała się w lokalnej strefie czasowej. Tj. wartość została przeanalizowana jako 2014-04-15 10:47:16+02:00 (przy założeniu, że lokalna strefa czasowa to UTC+2).

Nowe zachowanie

Począwszy od microsoft.Data.Sqlite 10.0, w przypadku używania GetDateTimeOffset znacznika czasu tekstowego, który nie ma przesunięcia, microsoft.Data.Sqlite przyjmie, że wartość jest w formacie UTC.

Dlaczego

Jest zgodne z zachowaniem SQLite, w którym znaczniki czasu bez przesunięcia są traktowane jako UTC.

Środki zaradcze

Należy odpowiednio dostosować kod.

W ostateczności/tymczasowo, można przywrócić poprzednie zachowanie, ustawiając Microsoft.Data.Sqlite.Pre10TimeZoneHandling przełącznik AppContext na true, zobacz AppContext dla konsumentów biblioteki aby uzyskać więcej szczegółów.

AppContext.SetSwitch("Microsoft.Data.Sqlite.Pre10TimeZoneHandling", isEnabled: true);

Zapisywanie wartości DateTimeOffset w kolumnie REAL jest teraz zapisywane w formacie UTC

Zagadnienie śledzenia nr 36195

Stare zachowanie

Wcześniej, podczas zapisywania wartości DateTimeOffset do kolumny REAL, Microsoft.Data.Sqlite zapisywał tę wartość, nie uwzględniając przesunięcia.

Nowe zachowanie

Począwszy od Microsoft.Data.Sqlite 10.0, podczas zapisywania wartości DateTimeOffset w kolumnie REAL, program Microsoft.Data.Sqlite przekonwertuje tę wartość na UTC przed dokonaniem konwersji i jej zapisem.

Dlaczego

Zapisana wartość była niepoprawna, a nie jest zgodna z zachowaniem sqLite, w którym znaczniki czasu rzeczywistego są asummowane jako UTC.

Środki zaradcze

Należy odpowiednio dostosować kod.

W ostateczności/tymczasowo, można przywrócić poprzednie zachowanie, ustawiając Microsoft.Data.Sqlite.Pre10TimeZoneHandling przełącznik AppContext na true, zobacz AppContext dla konsumentów biblioteki aby uzyskać więcej szczegółów.

AppContext.SetSwitch("Microsoft.Data.Sqlite.Pre10TimeZoneHandling", isEnabled: true);

Użycie funkcji GetDateTime z przesunięciem zwraca teraz wartość w formacie UTC

Zagadnienie śledzenia nr 36195

Stare zachowanie

Wcześniej, gdy używano GetDateTime na tekstowym znaczniku czasu z przesunięciem (np. 2014-04-15 10:47:16+02:00), Microsoft.Data.Sqlite zwracało wartość z DateTimeKind.Local (nawet jeśli przesunięcie nie było lokalne). Czas został poprawnie przeanalizowany, biorąc pod uwagę przesunięcie.

Nowe zachowanie

Począwszy od Microsoft.Data.Sqlite 10.0, w przypadku używania GetDateTime na tekstowym znaczniku czasu, który ma przesunięcie, Microsoft.Data.Sqlite przekonwertuje wartość na UTC i zwróci ją z DateTimeKind.Utc.

Dlaczego

Mimo że czas został poprawnie przeanalizowany, był zależny od lokalnej strefy czasowej skonfigurowanej przez maszynę, co może prowadzić do nieoczekiwanych wyników.

Środki zaradcze

Należy odpowiednio dostosować kod.

W ostateczności/tymczasowo, można przywrócić poprzednie zachowanie, ustawiając Microsoft.Data.Sqlite.Pre10TimeZoneHandling przełącznik AppContext na true, zobacz AppContext dla konsumentów biblioteki aby uzyskać więcej szczegółów.

AppContext.SetSwitch("Microsoft.Data.Sqlite.Pre10TimeZoneHandling", isEnabled: true);