Jak używać zestawu SDK serwera zaplecza platformy ASP.NET Core
W tym artykule pokazano, jak skonfigurować i użyć zestawu SDK serwera zaplecza platformy ASP.NET Core do utworzenia serwera synchronizacji danych.
Obsługiwane platformy
Serwer zaplecza platformy ASP.NET Core obsługuje ASP.NET 6.0 lub nowszym.
Serwery baz danych muszą spełniać następujące kryteria, mają DateTime
pole typu lub Timestamp
, które jest przechowywane z dokładnością milisekund. Implementacje repozytorium są udostępniane dla platform Entity Framework Core i LiteDb.
Aby uzyskać określoną obsługę bazy danych, zobacz następujące sekcje:
Tworzenie nowego serwera synchronizacji danych
Serwer synchronizacji danych używa normalnych mechanizmów ASP.NET Core do tworzenia serwera. Składa się z trzech kroków:
- Utwórz projekt serwera ASP.NET w wersji 6.0 (lub nowszej).
- Dodawanie programu Entity Framework Core
- Dodawanie usług synchronizacji danych
Aby uzyskać informacje na temat tworzenia usługi ASP.NET Core za pomocą programu Entity Framework Core, zobacz samouczek.
Aby włączyć usługi synchronizacji danych, należy dodać następujące biblioteki NuGet:
- Microsoft.AspNetCore.Datasync
- Microsoft.AspNetCore.Datasync.EFCore dla tabel opartych na programie Entity Framework Core.
- Microsoft.AspNetCore.Datasync.InMemory dla tabel w pamięci.
Zmodyfikuj plik Program.cs
. Dodaj następujący wiersz we wszystkich innych definicjach usługi:
builder.Services.AddDatasyncControllers();
Możesz również użyć szablonu ASP.NET Core datasync-server
:
# This only needs to be done once
dotnet new -i Microsoft.AspNetCore.Datasync.Template.CSharp
mkdir My.Datasync.Server
cd My.Datasync.Server
dotnet new datasync-server
Szablon zawiera przykładowy model i kontroler.
Tworzenie kontrolera tabeli dla tabeli SQL
Domyślne repozytorium używa platformy Entity Framework Core. Tworzenie kontrolera tabeli jest procesem trzyetapowym:
- Utwórz klasę modelu dla modelu danych.
- Dodaj klasę modelu do
DbContext
klasy dla aplikacji. - Utwórz nową
TableController<T>
klasę, aby uwidocznić model.
Tworzenie klasy modelu
Wszystkie klasy modeli muszą implementować wartość ITableData
. Każdy typ repozytorium ma abstrakcyjną klasę, która implementuje ITableData
element . Repozytorium Entity Framework Core używa elementu EntityTableData
:
public class TodoItem : EntityTableData
{
/// <summary>
/// Text of the Todo Item
/// </summary>
public string Text { get; set; }
/// <summary>
/// Is the item complete?
/// </summary>
public bool Complete { get; set; }
}
Interfejs ITableData
udostępnia identyfikator rekordu wraz z dodatkowymi właściwościami do obsługi usług synchronizacji danych:
UpdatedAt
(DateTimeOffset?
) zawiera datę ostatniej aktualizacji rekordu.Version
(byte[]
) udostępnia nieprzezroczystą wartość, która zmienia się na każdym zapisie.Deleted
(bool
) ma wartość true, jeśli rekord jest oznaczony do usunięcia, ale nie jest jeszcze czyszczony.
Biblioteka synchronizacji danych utrzymuje te właściwości. Nie modyfikuj tych właściwości we własnym kodzie.
Aktualizowanie DbContext
Każdy model w bazie danych musi być zarejestrowany w obiekcie DbContext
. Na przykład:
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
public DbSet<TodoItem> TodoItems { get; set; }
}
Tworzenie kontrolera tabeli
Kontroler tabeli jest wyspecjalizowanym ApiController
kontrolerem . Oto minimalny kontroler tabeli:
[Route("tables/[controller]")]
public class TodoItemController : TableController<TodoItem>
{
public TodoItemController(AppDbContext context) : base()
{
Repository = new EntityTableRepository<TodoItem>(context);
}
}
Uwaga
- Kontroler musi mieć trasę. Zgodnie z konwencją tabele są widoczne na ścieżce podrzędnej
/tables
elementu , ale można je umieścić w dowolnym miejscu. Jeśli używasz bibliotek klienckich starszych niż wersja 5.0.0, tabela musi być ścieżką podrzędną ./tables
- Kontroler musi dziedziczyć z
TableController<T>
klasy , gdzie<T>
jest implementacją implementacjiITableData
dla typu repozytorium. - Przypisz repozytorium na podstawie tego samego typu co model.
Implementowanie repozytorium w pamięci
Można również użyć repozytorium w pamięci bez magazynu trwałego. Dodaj pojedynczą usługę dla repozytorium w pliku Program.cs
:
IEnumerable<Model> seedData = GenerateSeedData();
builder.Services.AddSingleton<IRepository<Model>>(new InMemoryRepository<Model>(seedData));
Skonfiguruj kontroler tabeli w następujący sposób:
[Route("tables/[controller]")]
public class ModelController : TableController<Model>
{
public MovieController(IRepository<Model> repository) : base(repository)
{
}
}
Konfigurowanie opcji kontrolera tabeli
Niektóre aspekty kontrolera można skonfigurować przy użyciu polecenia TableControllerOptions
:
[Route("tables/[controller]")]
public class MoodelController : TableController<Model>
{
public ModelController(IRepository<Model> repository) : base(repository)
{
Options = new TableControllerOptions { PageSize = 25 };
}
}
Opcje, które można ustawić, obejmują:
PageSize
(int
wartość domyślna: 100) to maksymalna liczba elementów zwracanych przez operację zapytania na jednej stronie.MaxTop
(int
wartość domyślna: 512000) to maksymalna liczba elementów zwracanych w operacji zapytania bez stronicowania.EnableSoftDelete
(bool
, wartość domyślna: false) umożliwia usuwanie nietrwałe, co oznacza elementy jako usunięte zamiast usuwania ich z bazy danych. Usuwanie nietrwałe umożliwia klientom aktualizowanie pamięci podręcznej w trybie offline, ale wymaga oddzielnego przeczyszczania usuniętych elementów z bazy danych.UnauthorizedStatusCode
(int
wartość domyślna: 401 Brak autoryzacji) to kod stanu zwracany, gdy użytkownik nie może wykonać akcji.
Konfigurowanie uprawnień dostępu
Domyślnie użytkownik może wykonywać dowolne czynności, które chcą jednostki w tabeli — tworzyć, odczytywać, aktualizować i usuwać dowolny rekord. Aby uzyskać bardziej szczegółową kontrolę nad autoryzacją, utwórz klasę, która implementuje IAccessControlProvider
element . Metoda IAccessControlProvider
używa trzech metod do implementowania autoryzacji:
GetDataView()
Zwraca wartość lambda, która ogranicza to, co widzi połączony użytkownik.IsAuthorizedAsync()
Określa, czy połączony użytkownik może wykonać akcję dla określonej jednostki, która jest żądana.PreCommitHookAsync()
dostosowuje dowolną jednostkę bezpośrednio przed zapisaniem w repozytorium.
Między trzema metodami można skutecznie obsługiwać większość przypadków kontroli dostępu. Jeśli potrzebujesz dostępu do obiektu HttpContext
, skonfiguruj funkcję HttpContextAccessor.
Na przykład poniższa tabela implementuje tabelę osobistą, w której użytkownik może zobaczyć tylko własne rekordy.
public class PrivateAccessControlProvider<T>: IAccessControlProvider<T>
where T : ITableData
where T : IUserId
{
private readonly IHttpContextAccessor _accessor;
public PrivateAccessControlProvider(IHttpContextAccessor accessor)
{
_accessor = accessor;
}
private string UserId { get => _accessor.HttpContext.User?.Identity?.Name; }
public Expression<Func<T,bool>> GetDataView()
{
return (UserId == null)
? _ => false
: model => model.UserId == UserId;
}
public Task<bool> IsAuthorizedAsync(TableOperation op, T entity, CancellationToken token = default)
{
if (op == TableOperation.Create || op == TableOperation.Query)
{
return Task.FromResult(true);
}
else
{
return Task.FromResult(entity?.UserId != null && entity?.UserId == UserId);
}
}
public virtual Task PreCommitHookAsync(TableOperation operation, T entity, CancellationToken token = default)
{
entity.UserId == UserId;
return Task.CompletedTask;
}
}
Metody są asynchroniczne w przypadku, gdy musisz wykonać dodatkowe wyszukiwanie bazy danych, aby uzyskać poprawną odpowiedź. Interfejs można zaimplementować IAccessControlProvider<T>
na kontrolerze, ale nadal musisz przekazać element , IHttpContextAccessor
aby uzyskać bezpieczny dostęp do HttpContext
wątku.
Aby użyć tego dostawcy kontroli dostępu, zaktualizuj plik TableController
w następujący sposób:
[Authorize]
[Route("tables/[controller]")]
public class ModelController : TableController<Model>
{
public ModelsController(AppDbContext context, IHttpContextAccessor accessor) : base()
{
AccessControlProvider = new PrivateAccessControlProvider<Model>(accessor);
Repository = new EntityTableRepository<Model>(context);
}
}
Jeśli chcesz zezwolić zarówno na dostęp nieuwierzytelniony, jak i uwierzytelniony do tabeli, udekoruj go [AllowAnonymous]
zamiast [Authorize]
.
Konfigurowanie rejestrowania
Rejestrowanie jest obsługiwane za pomocą normalnego mechanizmu rejestrowania dla platformy ASP.NET Core. ILogger
Przypisz obiekt do Logger
właściwości:
[Authorize]
[Route("tables/[controller]")]
public class ModelController : TableController<Model>
{
public ModelController(AppDbContext context, Ilogger<ModelController> logger) : base()
{
Repository = new EntityTableRepository<Model>(context);
Logger = logger;
}
}
Monitorowanie zmian repozytorium
Po zmianie repozytorium można wyzwolić przepływy pracy, zarejestrować odpowiedź na klienta lub wykonać inną pracę w jednej z dwóch metod:
Opcja 1. Implementowanie narzędzia PostCommitHookAsync
Interfejs IAccessControlProvider<T>
udostępnia metodę PostCommitHookAsync()
. Metoda Th PostCommitHookAsync()
jest wywoływana po zapisaniu danych do repozytorium, ale przed zwróceniem danych do klienta. Należy zadbać o to, aby upewnić się, że dane zwracane do klienta nie są zmieniane w tej metodzie.
public class MyAccessControlProvider<T> : AccessControlProvider<T> where T : ITableData
{
public override async Task PostCommitHookAsync(TableOperation op, T entity, CancellationToken cancellationToken = default)
{
// Do any work you need to here.
// Make sure you await any asynchronous operations.
}
}
Użyj tej opcji, jeśli uruchamiasz zadania asynchroniczne w ramach haka.
Opcja 2. Użyj programu obsługi zdarzeń RepositoryUpdated
Klasa TableController<T>
bazowa zawiera procedurę obsługi zdarzeń, która jest wywoływana w tym samym czasie co PostCommitHookAsync()
metoda.
[Authorize]
[Route(tables/[controller])]
public class ModelController : TableController<Model>
{
public ModelController(AppDbContext context) : base()
{
Repository = new EntityTableRepository<Model>(context);
RepositoryUpdated += OnRepositoryUpdated;
}
internal void OnRepositoryUpdated(object sender, RepositoryUpdatedEventArgs e)
{
// The RepositoryUpdatedEventArgs contains Operation, Entity, EntityName
}
}
Włączanie tożsamości usługi aplikacja systemu Azure
Serwer synchronizacji danych ASP.NET Core obsługuje ASP.NET Core Identity lub dowolny inny schemat uwierzytelniania i autoryzacji, który chcesz obsługiwać. Aby ułatwić uaktualnianie z wcześniejszych wersji usługi Azure Mobile Apps, udostępniamy również dostawcę tożsamości, który implementuje tożsamość usługi aplikacja systemu Azure. Aby skonfigurować tożsamość usługi aplikacja systemu Azure w aplikacji, edytuj polecenie Program.cs
:
builder.Services.AddAuthentication(AzureAppServiceAuthentication.AuthenticationScheme)
.AddAzureAppServiceAuthentication(options => options.ForceEnable = true);
// Then later, after you have created the app
app.UseAuthentication();
app.UseAuthorization();
obsługa baz danych
Program Entity Framework Core nie konfiguruje generowania wartości dla kolumn daty/godziny. (Zobacz Generowanie wartości daty/godziny). Repozytorium usługi Azure Mobile Apps dla platformy Entity Framework Core automatycznie aktualizuje UpdatedAt
pole. Jeśli jednak baza danych jest aktualizowana poza repozytorium, należy zorganizować UpdatedAt
aktualizację pól i Version
.
Azure SQL
Utwórz wyzwalacz dla każdej jednostki:
CREATE OR ALTER TRIGGER [dbo].[TodoItems_UpdatedAt] ON [dbo].[TodoItems]
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
UPDATE
[dbo].[TodoItems]
SET
[UpdatedAt] = GETUTCDATE()
WHERE
[Id] IN (SELECT [Id] FROM INSERTED);
END
Ten wyzwalacz można zainstalować przy użyciu migracji lub natychmiast po EnsureCreated()
utworzeniu bazy danych.
Azure Cosmos DB
Usługa Azure Cosmos DB to w pełni zarządzana baza danych NoSQL na potrzeby aplikacji o wysokiej wydajności o dowolnym rozmiarze lub skali. Aby uzyskać informacje na temat korzystania z usługi Azure Cosmos DB z platformą Entity Framework Core, zobacz Dostawca usługi Azure Cosmos DB. W przypadku korzystania z usługi Azure Cosmos DB z usługą Azure Mobile Apps:
Skonfiguruj kontener cosmos za pomocą indeksu złożonego, który określa
UpdatedAt
pola iId
. Indeksy złożone można dodawać do kontenera za pośrednictwem witryny Azure Portal, usługi ARM, Bicep, narzędzia Terraform lub w kodzie. Oto przykładowa definicja zasobu bicep :resource cosmosContainer 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2023-04-15' = { name: 'TodoItems' parent: cosmosDatabase properties: { resource: { id: 'TodoItems' partitionKey: { paths: [ '/Id' ] kind: 'Hash' } indexingPolicy: { indexingMode: 'consistent' automatic: true includedPaths: [ { path: '/*' } ] excludedPaths: [ { path: '/"_etag"/?' } ] compositeIndexes: [ [ { path: '/UpdatedAt' order: 'ascending' } { path: '/Id' order: 'ascending' } ] ] } } } }
Jeśli ściągniesz podzbiór elementów w tabeli, upewnij się, że określono wszystkie właściwości związane z zapytaniem.
Tworzenie modeli z
ETagEntityTableData
klasy:public class TodoItem : ETagEntityTableData { public string Title { get; set; } public bool Completed { get; set; } }
Dodaj metodę
OnModelCreating(ModelBuilder)
do metodyDbContext
. Sterownik usługi Cosmos DB dla programu Entity Framework domyślnie umieszcza wszystkie jednostki w tym samym kontenerze. Co najmniej należy wybrać odpowiedni klucz partycji i upewnić się, żeEntityTag
właściwość jest oznaczona jako tag współbieżności. Na przykład poniższy fragment kodu przechowujeTodoItem
jednostki we własnym kontenerze z odpowiednimi ustawieniami usługi Azure Mobile Apps:protected override void OnModelCreating(ModelBuilder builder) { builder.Entity<TodoItem>(builder => { // Store this model in a specific container. builder.ToContainer("TodoItems"); // Do not include a discriminator for the model in the partition key. builder.HasNoDiscriminator(); // Set the partition key to the Id of the record. builder.HasPartitionKey(model => model.Id); // Set the concurrency tag to the EntityTag property. builder.Property(model => model.EntityTag).IsETagConcurrency(); }); base.OnModelCreating(builder); }
Usługa Azure Cosmos DB jest obsługiwana w pakiecie Microsoft.AspNetCore.Datasync.EFCore
NuGet od wersji 5.0.11. Aby uzyskać więcej informacji, zapoznaj się z następującymi linkami:
- Przykład usługi Cosmos DB.
- Dokumentacja dostawcy usługi Azure Cosmos DB platformy EF Core.
- Dokumentacja zasad indeksowania usługi Cosmos DB.
PostgreSQL
Utwórz wyzwalacz dla każdej jednostki:
CREATE OR REPLACE FUNCTION todoitems_datasync() RETURNS trigger AS $$
BEGIN
NEW."UpdatedAt" = NOW() AT TIME ZONE 'UTC';
NEW."Version" = convert_to(gen_random_uuid()::text, 'UTF8');
RETURN NEW
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE TRIGGER
todoitems_datasync
BEFORE INSERT OR UPDATE ON
"TodoItems"
FOR EACH ROW EXECUTE PROCEDURE
todoitems_datasync();
Ten wyzwalacz można zainstalować przy użyciu migracji lub natychmiast po EnsureCreated()
utworzeniu bazy danych.
Sqlite
Ostrzeżenie
Nie używaj sqlite dla usług produkcyjnych. SqLite nadaje się tylko do użycia po stronie klienta w środowisku produkcyjnym.
SqLite nie ma pola daty/godziny, które obsługuje dokładność milisekund. W związku z tym nie nadaje się do niczego, z wyjątkiem testowania. Jeśli chcesz użyć narzędzia SqLite, upewnij się, że implementujesz konwerter wartości i moduł porównujący wartości w każdym modelu dla właściwości daty/godziny. Najprostszą metodą implementowania konwerterów wartości i porównań jest OnModelCreating(ModelBuilder)
metoda :DbContext
protected override void OnModelCreating(ModelBuilder builder)
{
var timestampProps = builder.Model.GetEntityTypes().SelectMany(t => t.GetProperties())
.Where(p => p.ClrType == typeof(byte[]) && p.ValueGenerated == ValueGenerated.OnAddOrUpdate);
var converter = new ValueConverter<byte[], string>(
v => Encoding.UTF8.GetString(v),
v => Encoding.UTF8.GetBytes(v)
);
foreach (var property in timestampProps)
{
property.SetValueConverter(converter);
property.SetDefaultValueSql("STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')");
}
base.OnModelCreating(builder);
}
Zainstaluj wyzwalacz aktualizacji podczas inicjowania bazy danych:
internal static void InstallUpdateTriggers(DbContext context)
{
foreach (var table in context.Model.GetEntityTypes())
{
var props = table.GetProperties().Where(prop => prop.ClrType == typeof(byte[]) && prop.ValueGenerated == ValueGenerated.OnAddOrUpdate);
foreach (var property in props)
{
var sql = $@"
CREATE TRIGGER s_{table.GetTableName()}_{prop.Name}_UPDATE AFTER UPDATE ON {table.GetTableName()}
BEGIN
UPDATE {table.GetTableName()}
SET {prop.Name} = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')
WHERE rowid = NEW.rowid;
END
";
context.Database.ExecuteSqlRaw(sql);
}
}
}
Upewnij się, że metoda jest wywoływana InstallUpdateTriggers
tylko raz podczas inicjowania bazy danych:
public void InitializeDatabase(DbContext context)
{
bool created = context.Database.EnsureCreated();
if (created && context.Database.IsSqlite())
{
InstallUpdateTriggers(context);
}
context.Database.SaveChanges();
}
LiteDB
LiteDB to bezserwerowa baza danych dostarczana w pojedynczej małej biblioteki DLL napisanej w kodzie zarządzanym .NET C#. Jest to proste i szybkie rozwiązanie bazy danych NoSQL dla aplikacji autonomicznych. Aby użyć bazy danych LiteDb z magazynem trwałym na dysku:
Microsoft.AspNetCore.Datasync.LiteDb
Zainstaluj pakiet z narzędzia NuGet.Dodaj pojedynczy element dla elementu
LiteDatabase
do elementuProgram.cs
:const connectionString = builder.Configuration.GetValue<string>("LiteDb:ConnectionString"); builder.Services.AddSingleton<LiteDatabase>(new LiteDatabase(connectionString));
Tworzenie modeli pochodnych z elementu
LiteDbTableData
:public class TodoItem : LiteDbTableData { public string Title { get; set; } public bool Completed { get; set; } }
Możesz użyć dowolnych atrybutów dostarczanych
BsonMapper
z pakietem NuGet LiteDb.Utwórz kontroler przy użyciu polecenia
LiteDbRepository
:[Route("tables/[controller]")] public class TodoItemController : TableController<TodoItem> { public TodoItemController(LiteDatabase db) : base() { Repository = new LiteDbRepository<TodoItem>(db, "todoitems"); } }
Obsługa interfejsu OpenAPI
Interfejs API zdefiniowany przez kontrolery synchronizacji danych można opublikować przy użyciu narzędzia NSwag lub Swashbuckle. W obu przypadkach rozpocznij od skonfigurowania usługi tak, jak zwykle dla wybranej biblioteki.
NSwag
Postępuj zgodnie z podstawowymi instrukcjami dotyczącymi integracji sieciowej grupy zabezpieczeń, a następnie zmodyfikuj w następujący sposób:
Dodaj pakiety do projektu, aby obsługiwać aplikację NSwag. Wymagane są następujące pakiety:
Dodaj następujące elementy na początku
Program.cs
pliku:using Microsoft.AspNetCore.Datasync.NSwag;
Dodaj usługę, aby wygenerować definicję interfejsu OpenAPI do
Program.cs
pliku:builder.Services.AddOpenApiDocument(options => { options.AddDatasyncProcessors(); });
Włącz oprogramowanie pośredniczące do obsługi wygenerowanego dokumentu JSON i interfejsu użytkownika programu Swagger, również w pliku
Program.cs
:if (app.Environment.IsDevelopment()) { app.UseOpenApi(); app.UseSwaggerUI3(); }
Przejście do /swagger
punktu końcowego usługi internetowej umożliwia przeglądanie interfejsu API. Definicję interfejsu OpenAPI można następnie zaimportować do innych usług (takich jak Usługa Azure API Management). Aby uzyskać więcej informacji na temat konfigurowania sieciowej grupy zabezpieczeń, zobacz Rozpoczynanie pracy z siecią NSwag i ASP.NET Core.
Swashbuckle
Postępuj zgodnie z podstawowymi instrukcjami dotyczącymi integracji pakietu Swashbuckle, a następnie zmodyfikuj w następujący sposób:
Dodaj pakiety do projektu, aby obsługiwać pakiet Swashbuckle. Wymagane są następujące pakiety:
Dodaj usługę, aby wygenerować definicję interfejsu OpenAPI do
Program.cs
pliku:builder.Services.AddSwaggerGen(options => { options.AddDatasyncControllers(); }); builder.Services.AddSwaggerGenNewtonsoftSupport();
Uwaga
Metoda
AddDatasyncControllers()
przyjmuje opcjonalny elementAssembly
odpowiadający zestawowi zawierającemu kontrolery tabel. ParametrAssembly
jest wymagany tylko wtedy, gdy kontrolery tabel znajdują się w innym projekcie usługi.Włącz oprogramowanie pośredniczące do obsługi wygenerowanego dokumentu JSON i interfejsu użytkownika programu Swagger, również w pliku
Program.cs
:if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(options => { options.SwaggerEndpoint("/swagger/v1/swagger.json", "v1"); options.RoutePrefix = string.Empty; }); }
Dzięki tej konfiguracji przejście do katalogu głównego usługi internetowej umożliwia przeglądanie interfejsu API. Definicję interfejsu OpenAPI można następnie zaimportować do innych usług (takich jak Usługa Azure API Management). Aby uzyskać więcej informacji na temat konfigurowania pakietu Swashbuckle, zobacz Wprowadzenie do programu Swashbuckle i ASP.NET Core.
Ograniczenia
Wersja ASP.NET Core bibliotek usług implementuje operację listy OData w wersji 4. Gdy serwer działa w trybie zgodności z poprzednimi wersjami, filtrowanie podciągów nie jest obsługiwane.
Opinia
https://aka.ms/ContentUserFeedback.
Dostępne już wkrótce: W 2024 r. będziemy stopniowo wycofywać zgłoszenia z serwisu GitHub jako mechanizm przesyłania opinii na temat zawartości i zastępować go nowym systemem opinii. Aby uzyskać więcej informacji, sprawdź:Prześlij i wyświetl opinię dla