Migracje w Entity Framework

0

Cześć. Mam problem z migracjami. Mam w solucji projekty:

  • Domain
  • UI

w projekcie Domain mam miedzy innymi modele i context natomiast w UI mam kontekst dla Identity. Jak utworze migracje w projekcie UI to tworza mi sie w bazie tabele powiazane z Identity ale nie ma tych z klas modelu z projektu Domain. O co chodzi? Czy mogracje nie powinny byc w projekcie startowym?

0

Jest tak dla tego ze migracje bazują na aktualnym kontekście. W Twoim przypadku jest to kontekst odpowiadający za Identity. Masz dwa oddzielne konteksty a więc musisz mieć dwa oddzielne zestawy migracji.

Natomiast ogólnie jest to zle podejście. Dobrze zaczales dając część do innego projektu, natomiast dla czego zostawiłeś Identity w UI? Domyślam się że przez potrzebę babrania się z własną implementacją? Najlepiej jakbyś wszystko dał razem do jednego projektu, szczególnie do oddzielnego od Domain skoro już ładne chcesz sobie to podzielić. Dostęp do bazy to nie domena a infrastruktura.

Co do Identity to gdzieś ostatnio wklejalem link do mojego GitHuba gdzie napisałem prosty przykład odłaczenia Identity od projektu UI, i od konkretnej i implentacji perzystencji. Spróbuję znaleźć tamten wątek, może Ci się przyda.

EDIT: https://4programmers.net/Forum/1513882

0

@Aventus: widziałem twoj kod. Fajnie to wyglada. Probuje to jakos sobie zobrazowac i dopasowac do mojego przypadku. Powiedz mi czy dobrze mysle. Moje klasy ktore mapuja sie na tabele do bazy dałbym sobie jako osobny projekt. A moja klasa dziedziczaca po DbContext powedrowala by do twojego projektu InMemory. Tam tez znalazly by sie Migracje. Dobrze kombinuje?

1

Tak, klasy modelowe jako osobny projekt (lub po prostu w Domain), natomiast context tam gdzie InMemory. Oczywiście to już nie będzie in memory :) będziesz miał więc jeden Context do wszystkiego. No chyba że chcesz specjalnie mieć infrastrukturę od Identity osobną ale to już inna zabawa.

Weź pod uwagę że mając Context w bibliotece klas zamiast w projekcie takim jak Web czy aplikacja konsolowa musisz dostarczyć trochę wspierającego kodu aby EF wiedziało skąd tworzyć migracje- samo wskazanie projektu nie wystarczy. Po szczegóły odsyłam tutaj: https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/DbContext-creation Konkretnie do design-time factory.

0

@Aventus dodałem sobie dodatkowy projekt - Infrastructure. Nie korzystam z DI wiec bede musiał to sobie inaczej zorganizowac.Wrzuciłem do Infrastructure ten context,który miałem do tej pory czyli ProductContext.
Tak jak sugerowałeś chce tu też dodać context powiazany z Identity. Mam tylko wątpliwość czy "wpleść" go do ProductContext czy dołożyć klasę z drugim konteksem:

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
   {
       public ApplicationDbContext()
           : base("ProductsConnection", throwIfV1Schema: false)
       {
       }

       public static ApplicationDbContext Create()
       {
           return new ApplicationDbContext();
       }
   }

Obydwa konteksty operowały by na tym samym połączeniu ProductConnection zebym miał wszystko w jednej bazie.

Chyba ze jestes w stanie mi podpowiedziec jak utworzyc kontekst dla tego twojego Identity zeby mi to do bazy zapisywało.

Chyba to nieskladnie napisałem bo sie pogubiłem :( Chce miec jeden Context do wszystkiego, tylko nie bardzo wiem wrzucic te twoje klasy InMemory do mojego Contextu.

1

Faktycznie troche sie pogubiles :) A raczej podchodzisz do tego od zlej strony. To nie "moje klasy" maja byc w Twoim kontekscie tylko na odwrot- implementacje IRoleStore, IUserRoleStore oraz IUserPasswordStore maja korzystac z Twojego kontekstu. Innymi slowy- klasy zapewniajace role, uzytkownikow i obsluge hasel musza korzystac z jakiegos zrodla danych.W zwiazku z tym albo wstrzykuj w te klasy Twoj kontekst, albo tworz w nich nowe instancje kontekstu kiedy musisz wyciagnac jakies dane (np. znalezc uzytkownika w metodzie FindByIdAsync).

Swoja droga pozwol ze sie czepne- metoda Create() w Twoim konktescie jest troche bez sensu bo przeciez wystawiasz publiczny konstruktor.

Poza tym moge spytac czemu nie chcesz wstrzykiwac zaleznosci?

Aha, no i skoro chcesz miec wlasna implementacje Identity to Twoj kontekst nie musi dziedziczyc po IdentityDbContext. W tym momencie Context jest niczym innym jak zrodlem danych- nie ma nic wspolnego z Identity.

0
Aventus napisał(a):

Poza tym moge spytac czemu nie chcesz wstrzykiwac zaleznosci?

Zle sie wyraziłem :) Zaleznosci chce wstrzykiwac ale nie chce na razie korzystac z zadnego kontenera. Za mało jeszcze umiem i kontenery tylko zaciemnia mi nauke :)

@Aventus: jesli moge jeszcze poprosic o pomoc :)
Przeorganizowałem sobie projekt i mam teraz projekty(robie aplikacje WebForms do tworzenia zamówień i skorzystałem z gotowego projektu w VS, który sobie modyfikuje)

  • Domain: tu mam modele, np Product, Order, OrderItem...
  • Infrastructure: tu mam utworzony kontekst z któego korzystam OrderAppContext, tutaj tez jest referencja do entity framework
  • **WebUI **- tutaj mam wizualną część

Mam wątpliwości:

1.

  • zawartość twojego projektu DecoupledIdentity.Core chciałbym umieścić w moim projekcie Domain
  • zawartość twojego projektu DecoupledIdentity.InMemory chcę wrzucić do mojego Infrastructure

Czy dobrze robię wrzucając to tak jak opisałem?

2.
Przejdzmy do klasy kontekstu OrderAppContext w projekcie Infrastructure. O ile aktualnie w klasie kontekstu mam coś w rodzaju

public class OrderAppContext : DbContext
    {
        public OrderAppContext() : base("OrdersConnection")
        {
           
        }

        public DbSet<Order> Orders { get; set; }
        public DbSet<OrderItem> OrderItems { get; set; }
        public DbSet<Product> Products { get; set; }
    }

to nie mam pojęcia jak tu ogarnąć sprawy Identity. Czy miałbym tu po prostu dodać:
```csharp
public DbSet<Role> Roles { get; set; }
public DbSet<User> Users { get; set; }

czy powinno to być zupełnie coś innego?
0
goodfather napisał(a):

1.
(...)
Czy dobrze robię wrzucając to tak jak opisałem?

Tak

2.
Przejdzmy do klasy kontekstu OrderAppContext w projekcie Infrastructure. O ile aktualnie w klasie kontekstu mam coś w rodzaju
(...)
to nie mam pojęcia jak tu ogarnąć sprawy Identity. Czy miałbym tu po prostu dodać:
```csharp
public DbSet<Role> Roles { get; set; }
public DbSet<User> Users { get; set; }

czy powinno to być zupełnie coś innego?

Tak jak opisales, po prostu umieść wszystko w jednym kontekście. W końcu User i Role to też część Twojej domeny.

0

@Aventus, zrobiłem tak jak pisałeś

public class OrderAppContext : DbContext
    {
        public OrderAppContext() : base("OrdersConnection")
        {
 
        }
 
        public DbSet<Order> Orders { get; set; }
        public DbSet<OrderItem> OrderItems { get; set; }
        public DbSet<Product> Products { get; set; }
        public DbSet<Role> Roles { get; set; }
        public DbSet<User> Users { get; set; }
    }

Do projektu Infrastructure dodałem migracje. Teraz dostaję komunikaty, że nie można utworzyć migracji, bo w klasach Role i User nie zdefiniowano klucza. Rozumiem ze mam dodać ten klucz. Pytanie czy w tabeli User kluczem ma być np UserName bo to będzie raczej unikalna wartość. Co ma być kluczem w tabeli Role? Nazwa roli, czy tworzyć sztuczny klucz?

1

Nigdy nie używaj wartości "naturalnych" (nie wiem czy to dobre określenie) jako kluczy. Użyj inta albo Guid. Nic nie stoi na przeszkodzie abyś wymusił unikalność nazwy użytkownika- po prostu nadaj właściwości index. Używając model builder: builder.Entity<User>().HasIndex(u => u.Username).IsUnique().

0
Aventus napisał(a):

Nigdy nie używaj wartości "naturalnych" (nie wiem czy to dobre określenie) jako kluczy. Użyj inta albo Guid.

Z preferencją dla int, ewentualnie long, jeśli rekoredów może być dużo. Guidy tylko, gdy importujesz z wielu źródeł danych, i może nastąpić kolizja kluczy liczbowych. (Czyli nigdy w CRUDach, w których do bazy pisze tylko jedna aplikacja/API.)

0

@Aventus

A jaki argument za waleniem guidów gdzie popadnie?

1

Ale o co chodzi? Argumenty zawsze się znajdą, za jednym i za drugim. Spytałem z ciekawości i odpowiedź uzyskałem (w odróżnieniu od Twojej błyskotliwej odpowiedzi).

1
Aventus napisał(a):

Ale o co chodzi? Argumenty zawsze się znajdą, za jednym i za drugim. Spytałem z ciekawości i odpowiedź uzyskałem (w odróżnieniu od Twojej błyskotliwej odpowiedzi).

Źle cię zinterpretowałem.

0
Aventus napisał(a):

Używając model builder: builder.Entity<User>().HasIndex(u => u.Username).IsUnique().

  1. Robię aplikację **WebForms **i chyba nie ma u mnie czegoś takiego jak model builder.
  2. W **CoreEntity **jest zdefiniowane Id, które jest dziedziczone przez **Role **i User. Czy to jest mój klucz główny który powinien zostac zmapowany na klucz główny w bazie?
1

Tak. Poza tym nie ma znaczenia jaki to rodzaj aplikacji skoro uzywasz Entity Framework. Moj blad bo zapomnialem wspomniec ze w swoim kontekscie musisz nadpisac OnModelCreating. Tutaj wiecej wyjasnione zarowno dla EF6 jak i EF Core:

http://www.entityframeworktutorial.net/code-first/move-configurations-to-seperate-class-in-code-first.aspx
https://www.learnentityframeworkcore.com/configuration/fluent-api

1

Przerobiłem klasę kontekstu dodając:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            //base.OnModelCreating(modelBuilder);
            modelBuilder.Entity<User>().HasKey(u => u.Id);
            modelBuilder.Entity<User>().HasIndex(u => u.UserName).IsUnique();
            modelBuilder.Entity<Role>().HasKey(r => r.Id);
            modelBuilder.Entity<Role>().HasIndex(r => r.Name).IsUnique();
        }

Chyba tak to powinno wyglądać :)
Zabieram się dalsze przerabianie kodu :)

W linkach które mi podałeś jest konfiguracja za pomocą fluent api. Czy dzięki temu osiągam taki sam efekt jak przy uzywaniu atrybutów w klasach modeli?
Np jest tam przykład

this.Property(p => p.StudentName)
                    .HasMaxLength(50);

czy to da ten sam efekt co

class Student
{
   [StringLength(50)]
   public string StudentName{get;set;}
//...
}
2

Dokładnie tak. Osobiście jestem zwolennikiem nie używania atrybutów w ogóle. Po pierwsze dla tego ze atrybuty są ograniczone i w pewnym momencie może się okazać że będzie trzeba uzyc fluent API tak czy inaczej. W rezultacie zostaniemy z pomieszanym kodem- gdzieś bedzie konfiguracja poprzez atrybuty, gdzie indziej fluent api. Po drugie atrybuty wiążą model z warstwą perzystencji, a model na ten temat nie powinien nic "wiedzieć".

Co do samej konfiguracji w modelbuilder-sprawdź dobrze linki które Ci wysłałem. Jest tam pokazane jak rozdzielić konfigurację i mieć dedykowaną klasę konfiguracji dla każdego modelu.

Poza tym nie powinno być potrzeby jawnego deklarowania właściwości Id jako klucza. EF domyślnie wyszukuje właściwości Id lub [nazwa klasy]Id i traktuje je jako klucz.

0

Tabele Role i User już mi się tworzą w bazie :), ale utknąłem w innym miejscu :( Mianowicie nie wiem jak zrobić tą konfigurację która u ciebie jest w IdentityConfiguration.

Ja używam predefiniowanego szablonu aplikacji WebForms i mam tam taki kod wygenerowany przez Visual Studio:

namespace WebUI
{
   

    // Configure the application user manager used in this application. UserManager is defined in ASP.NET Identity and is used by the application.
    public class ApplicationUserManager : UserManager<ApplicationUser>
    {
        public ApplicationUserManager(IUserStore<ApplicationUser> store)
            : base(store)
        {
        }

        public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
        {
            var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<ApplicationDbContext>()));
            // Configure validation logic for usernames
            manager.UserValidator = new UserValidator<ApplicationUser>(manager)
            {
                AllowOnlyAlphanumericUserNames = false,
                RequireUniqueEmail = true
            };

            // Configure validation logic for passwords
            manager.PasswordValidator = new PasswordValidator
            {
                RequiredLength = 6,
                RequireNonLetterOrDigit = true,
                RequireDigit = true,
                RequireLowercase = true,
                RequireUppercase = true,
            };

            // Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user
            // You can write your own provider and plug it in here.
            manager.RegisterTwoFactorProvider("Phone Code", new PhoneNumberTokenProvider<ApplicationUser>
            {
                MessageFormat = "Your security code is {0}"
            });
            manager.RegisterTwoFactorProvider("Email Code", new EmailTokenProvider<ApplicationUser>
            {
                Subject = "Security Code",
                BodyFormat = "Your security code is {0}"
            });

            // Configure user lockout defaults
            manager.UserLockoutEnabledByDefault = true;
            manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
            manager.MaxFailedAccessAttemptsBeforeLockout = 5;

          
            var dataProtectionProvider = options.DataProtectionProvider;
            if (dataProtectionProvider != null)
            {
                manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));
            }
            return manager;
        }
    }

    public class ApplicationSignInManager : SignInManager<ApplicationUser, string>
    {
        public ApplicationSignInManager(ApplicationUserManager userManager, IAuthenticationManager authenticationManager) :
            base(userManager, authenticationManager) { }

        public override Task<ClaimsIdentity> CreateUserIdentityAsync(ApplicationUser user)
        {
            return user.GenerateUserIdentityAsync((ApplicationUserManager)UserManager);
        }

        public static ApplicationSignInManager Create(IdentityFactoryOptions<ApplicationSignInManager> options, IOwinContext context)
        {
            return new ApplicationSignInManager(context.GetUserManager<ApplicationUserManager>(), context.Authentication);
        }
    }
}

Nie wiem jak to przerobić pod siebie. Czy **ApplikationUser **to będzie moja klasa User?

Tutaj też mam problem:

var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<ApplicationDbContext>()));

próbuję to przerobić pod moje klasy, ale to nie działa

var manager = new ApplicationUserManager(new UserStore<User>(context.Get<OrderAppContext>()));
0

Ja bym pominął klasy ApplicationSignInManager, ApplicationUserManager tylko tworzy obiekty UserManager<User> oraz SignInManager<User>. Jestem na etapie, że zarejestrowałem je w kontenerze IOC. Wstrzykuje do kontrolera i działa. Ale też przyłączam się do pytania jak zrobić te konfigurację

0

Ja nie używam kontenera i się to jeszcze bardziej komplikuje :(

0

Tak, Twoja klase User podstawiasz za ApplicationUser. Tam gdzie tworzysz instancje UserManager musisz po prostu przekazac w kontrolerze Twoj UserStore bo to wlasnie UserManager uzywa Store aby pobierac i tworzyc uzytkownikow.

0

Coś sobie pozmieniałem, ale znowu utknąłem

public static ApplicationUserManager Create(IdentityFactoryOptions<Infrastructure.Identity.UserStore> options, IOwinContext context)
{
var manager = new ApplicationUserManager(new Infrastructure.Identity.UserStore(new Infrastructure.Identity.RoleStore()));
//...

Dostaję błąd:
screenshot-20180918112823.png

1

Udało mi się przeskoczyć powyższy błąd.
Zmieniłem, aby UserStore implementował też interfejs IUserStore<>:

public sealed class UserStore : Store<User>, IUserPasswordStore<User>, IUserRoleStore<User>, IUserStore<User>
0

I kolejny problem :)

w linijce

app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);

pojawia się błąd:
screenshot-20180918155439.png

Moja klasa ApplicationUserManager z metodą **Create **wygląda następująco:

    public class ApplicationUserManager : UserManager<User>
    {
        public ApplicationUserManager(IUserStore<User> store)
            : base(store)
        {
        }

        public static ApplicationUserManager Create(IdentityFactoryOptions<Infrastructure.Identity.UserStore> options, IOwinContext context)
        {

            var manager = new ApplicationUserManager(new Infrastructure.Identity.UserStore(new Infrastructure.Identity.RoleStore()));
            manager.UserValidator = new UserValidator<User>(manager)
            {
                AllowOnlyAlphanumericUserNames = false,
                RequireUniqueEmail = true
            };

            manager.PasswordValidator = new PasswordValidator
            {
                RequiredLength = 6,
                RequireNonLetterOrDigit = true,
                RequireDigit = true,
                RequireLowercase = true,
                RequireUppercase = true,
            };

          
            manager.UserLockoutEnabledByDefault = true;
            manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
            manager.MaxFailedAccessAttemptsBeforeLockout = 5;

            manager.EmailService = new EmailService();
            manager.SmsService = new SmsService();
            var dataProtectionProvider = options.DataProtectionProvider;
            if (dataProtectionProvider != null)
            {
                manager.UserTokenProvider = new DataProtectorTokenProvider<User>(dataProtectionProvider.Create("ASP.NET Identity"));
            }
            return manager;
        }
    }

Co oznacza ten błąd?

0

Chyba przekazujesz metodę zamiast ja wywołać żeby przekazać wynik. Zamień ApplicationUserManager.Create na ApplicationUserManager.Create(). Strzelam w ciemno bo nie jestem pewny, napisz czy działa.

0

Nie pomaga, bo jeśli chcę wywołać metodę **Create **to muszę podać jej parametry

Create(IdentityFactoryOptions<Infrastructure.Identity.UserStore> options, IOwinContext context)

a klasa Startup nie ma dostępu do obiektów które mógłbym przekazać jako parametry.

1 użytkowników online, w tym zalogowanych: 0, gości: 1