This is an old revision of the document!
Scopul acestui laborator este de a va familiariza cu un concept foarte important in dezvoltarea aplicatiilor in general, anume Dependency injection, dar de asemenea sa va familiazati si interactiunea cu baza de date folosind un ORM.
Un motiv pentru care limbaje cum sunt Java si C# sunt populare pentru dezvoltarea de aplicatii este faptul ca suporta reflexie la runtime, adica programul la runtime poate face introspectie si poate, de exemplu, sa creeze instante de obiecte fara sa fie explicit programat in acest sens.
Acest lucru a ajutat la implementarea de dependency injection in aceste limbaje ce se rezuma doar la faptul ca la runtime componentele sunt instantiate rand pe rand de la cele mai simple la cele mai complexe iar instantele componenteleor sunt pasate ca paramentri pentru instantierea altor componente.
In exemplul de mai jos se poate vedea cum este declaranta o componeta, parametri dati la constructor sunt pasati de framework atunci cand se cere aceasta componenta, observati ca parametri sunt interfete. De obicei se injecteaza interfete, nu implementari efective motivul fiind ca pot exista mai multe implementari pentru o interfata care pot fi schimbate in functie de necesitati cum ar fi pentru testare, de exemplu se poate inlocui implementarea de productie cu una de test pentru interceptarea apelurilor de metode ale acelui serviciu.
public class InitializerWorker : BackgroundService { private readonly ILogger<InitializerWorker> _logger; private readonly IServiceProvider _serviceProvider; public InitializerWorker(ILogger<InitializerWorker> logger, IServiceProvider serviceProvider) { _logger = logger; \\ The logger instance is injected here. _serviceProvider = serviceProvider; \\ Here the service provider is injected to request other components on runtime at request. } protected override async Task ExecuteAsync(CancellationToken cancellationToken) { try { await using var scope = _serviceProvider.CreateAsyncScope(); \\ Here a new scope is created, this is useful to get new scoped instances. var userService = scope.ServiceProvider.GetService<IUserService>(); \\ Here an instance for a service is requested, \\ it may fail if the component is not declared or an exception \\ is thrown on it’s creation. if (userService == null) { _logger.LogInformation("Couldn't create the user service!"); return; } var count = await userService.GetUserCount(cancellationToken); if (count.Result == 0) { _logger.LogInformation("No user found, adding default user!"); await userService.AddUser(new() { Email = "admin@default.com", Name = "Admin", Role = UserRoleEnum.Admin, Password = PasswordUtils.HashPassword("default") }, cancellationToken: cancellationToken); } } catch (Exception ex) { _logger.LogError(ex, "An error occurred while initializing database!"); } } }
De mentionat, exista 3 tipuri de lifetime pentru instantele componentelor, anume:
Mai jos este un exemplu de cum se pot declara serviciile ca si componente transiente, fiecare serviciu implementeaza propria interfata, intefata practic este cheia de cautare pentru un serviciu. Serviciile sunt declarate adaugandu-le la configurarea builder-ului de applicatie in colectia de servicii.
builder.Services .AddTransient<IUserService, UserService>() .AddTransient<ILoginService, LoginService>() .AddTransient<IFileRepository, FileRepository>() .AddTransient<IUserFileService, UserFileService>() .AddTransient<IMailService, MailService>();
Pe langa logica aplicatiei, si vom discuta la laboratoarele urmatoare cum se implementeaza, trebuie sa existe persistenta datelor peste care se efectueaza logica efectiva. In acest sens majoritatea aplicatiilor folosesc baze de date, pentru a simplifica interactiunea programelor cu baza de date au fost implementate ORM-uri (Object-relational mapping), acestea sunt framework-uri care fac o corespondenta a tabelelor si a tipurilor de date din baza de date cu obiectele, numite entitati, si tipurile declarate in codul aplicatiei.
ORM-urile expun in general o interfata generica care poate fi folosita pentru mai multe baze de date (PostgreSQL, MariaDB sau SQLServer) folosind acelasi cod chiar daca pot exista particularizari pentru fiecare.
Mai jos aveti un exemplu de corespondenta a unor entati cu tabele din baza de date.
public abstract class BaseEntity { public Guid Id { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; public void UpdateTime() => UpdatedAt = DateTime.UtcNow; } public class User : BaseEntity { public string Name { get; set; } = default!; public string Email { get; set; } = default!; public string Password { get; set; } = default!; public UserRoleEnum Role { get; set; } = default!; public ICollection<UserFile> UserFiles { get; set; } = default!; } public class UserFile : BaseEntity { public string Path { get; set; } = default!; public string Name { get; set; } = default!; public string? Description { get; set; } public Guid UserId { get; set; } public User User { get; set; } = default!; }
Pentru fiecare entitate se creaza si o clasa de configurare pentru ca ORM-ul sa stie aditional diverse detalii cum ar fi ce proprietati corespund la chei primare, unice sau de referinta.
public class UserConfiguration : IEntityTypeConfiguration<User> { public void Configure(EntityTypeBuilder<User> builder) { builder.Property(e => e.Id) // This specifies which property is configured. .IsRequired(); // Here it is specified if the property is required, meaning it cannot be null in the database. builder.HasKey(x => x.Id); // Here it is specifies that the property Id is the primary key. builder.Property(e => e.Name) .HasMaxLength(255) // This specifies the maximum length for varchar type in the database. .IsRequired(); builder.Property(e => e.Email) .HasMaxLength(255) .IsRequired(); builder.HasAlternateKey(e => e.Email); // Here it is specifies that the property Email is a unique key. builder.Property(e => e.Password) .HasMaxLength(255) .IsRequired(); builder.Property(e => e.Role) .HasMaxLength(255) .IsRequired(); builder.Property(e => e.CreatedAt) .IsRequired(); builder.Property(e => e.UpdatedAt) .IsRequired(); } } public void Configure(EntityTypeBuilder<UserFile> builder) { builder.Property(e => e.Id) .IsRequired(); builder.HasKey(x => x.Id); builder.Property(e => e.Path) .HasMaxLength(255) .IsRequired(); builder.Property(e => e.Name) .HasMaxLength(255) .IsRequired(); builder.Property(e => e.Description) .HasMaxLength(4095) .IsRequired(false); // This specifies that this column can be null in the database. builder.Property(e => e.CreatedAt) .IsRequired(); builder.Property(e => e.UpdatedAt) .IsRequired(); builder.HasOne(e => e.User) // This specifies a one-to-many relation. .WithMany(e => e.UserFiles) // This provides the reverse mapping for the one-to-many relation. .HasForeignKey(e => e.UserId) // Here the foreign key column is specified. .HasPrincipalKey(e => e.Id) // This specifies the referenced key in the referenced table. .IsRequired() .OnDelete(DeleteBehavior.Cascade); // This specifies the delete behavior when the referenced entity is removed. } }
Cand se implementeaza o schema de baza de date aceasta poate suferii diverse modificari in timpul dezvoltarii si maturizarii aplicatiei. Din acesta cauza, modificarile pe schema de baza de date trebuie sa fie efectuate incremental, adica orice modificare se aplica peste modificarile precedente, si de aceia exista conceptul de migrare. O migrare este o transformare, adesea reversibila, a schemei bazei de date care sa reflecte schimbarile din cod. In Entity Framework puteti folosi migrari instaland dotnet-ef:
dotnet tool install --global dotnet-ef --version 6.*
O data ce au fost create entitatile si configurate corespunzator (vedeti configurarile pentru entitati din din proiectul laboratorului) se poate rula comanda de generare a migrarilor avand baza de date deschisa:
dotnet ef migrations add <nume_migrare> --context <nume_clasa_context> --project <proiect_cu_migrarile> --startup-project <proiect_cu_startup>