diff --git a/docs/Application-Services.md b/docs/Application-Services.md new file mode 100644 index 0000000000..3dc24c9d9a --- /dev/null +++ b/docs/Application-Services.md @@ -0,0 +1,3 @@ +## Application Services + +TODO \ No newline at end of file diff --git a/docs/Tutorials/AspNetCore-Mvc/Part-I.md b/docs/Tutorials/AspNetCore-Mvc/Part-I.md index 9b3c968692..4d68689626 100644 --- a/docs/Tutorials/AspNetCore-Mvc/Part-I.md +++ b/docs/Tutorials/AspNetCore-Mvc/Part-I.md @@ -1,3 +1,104 @@ ## ASP.NET Core MVC Tutorial - Part I -TODO \ No newline at end of file +> This tutorial assumes that you have created a new project, named `Acme.BookStore` from [the startup templates](https://abp.io/Templates). + +### About the Tutorial + +In this tutorial series, you will build an application that is used to manage a list of books & their authors. **Entity Framework Core** (EF Core) will be used as the ORM provider (as it comes pre-configured with the startup template). + +### Solution Structure + +This is the layered solution structure created from the startup template: + +![bookstore-visual-studio-solution](../../images/bookstore-visual-studio-solution.png) + +### Create the Book Entity + +Define [entities](../../Entities.md) in the **domain layer** (`Acme.BookStore.Domain` project) of the solution. The main entity of the application is the `Book`: + +````C# +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Volo.Abp.Domain.Entities; + +namespace Acme.BookStore +{ + [Table("Books")] + public class Book : AggregateRoot + { + [Required] + [StringLength(128)] + public string Name { get; set; } + + public BookType Type { get; set; } + + public DateTime PublishDate { get; set; } + + public float Price { get; set; } + } +} +```` + +* ABP has two fundamental base classes for entities: `AggregateRoot` and `Entity`. **Aggregate Roots** are one of the concepts of the **Domain Driven Design (DDD)**. See [entity document](../../Entities.md) for details and best practices. +* Used **data annotation attributes** in this code. You could use EF Core's [fluent mapping API](https://docs.microsoft.com/en-us/ef/core/modeling) instead. + +#### BookType Enum + +The `BookType` enum used above is defined as below: + +````C# +namespace Acme.BookStore +{ + public enum BookType : byte + { + Undefined, + Advanture, + Biography, + Dystopia, + Fantastic, + Horror, + Science, + ScienceFiction, + Poetry + } +} +```` + +#### Add Book Entity to Your DbContext + +EF Core requires to relate entities with your DbContext. The easiest way is to add a `DbSet` property to the `BookStoreDbContext` as shown below: + +````C# +public class BookStoreDbContext : AbpDbContext +{ + public DbSet Book { get; set; } + ... +} +```` + +* `BookStoreDbContext` is located in the `Acme.BookStore.EntityFrameworkCore` project. + +#### Add new Migration & Update the Database + +Startup template uses [EF Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/) to create and maintain the database schema. Open the **Package Manager Console (PMC)**, select the `Acme.BookStore.EntityFrameworkCore` as the **default project** and execute the following command: + +![bookstore-pmc-add-book-migration](../../images\bookstore-pmc-add-book-migration.png) + +This will create a new migration class inside the `Migrations` folder. Then execute the `Update-Database` command to update the database schema: + +```` +PM> Update-Database +```` + +#### Add Sample Data + +`Update-Database` command created the `Books` table in the database. Enter a few sample rows, so you can show them on the page: + +![bookstore-books-table](../../images\bookstore-books-table.png) + +### Create the Application Service + +The next step is to create an [application service](../../Application-Services.md) to manage (create, list, update, delete...) books. + +TODO... \ No newline at end of file diff --git a/docs/images/bookstore-books-table.png b/docs/images/bookstore-books-table.png new file mode 100644 index 0000000000..0493791918 Binary files /dev/null and b/docs/images/bookstore-books-table.png differ diff --git a/docs/images/bookstore-pmc-add-book-migration.png b/docs/images/bookstore-pmc-add-book-migration.png new file mode 100644 index 0000000000..5d29f4d32b Binary files /dev/null and b/docs/images/bookstore-pmc-add-book-migration.png differ diff --git a/samples/BookStore/src/Acme.BookStore.Domain/Book.cs b/samples/BookStore/src/Acme.BookStore.Domain/Book.cs new file mode 100644 index 0000000000..be5292aee3 --- /dev/null +++ b/samples/BookStore/src/Acme.BookStore.Domain/Book.cs @@ -0,0 +1,21 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Volo.Abp.Domain.Entities; + +namespace Acme.BookStore +{ + [Table("Books")] + public class Book : AggregateRoot + { + [Required] + [StringLength(128)] + public string Name { get; set; } + + public BookType Type { get; set; } + + public DateTime PublishDate { get; set; } + + public float Price { get; set; } + } +} diff --git a/samples/BookStore/src/Acme.BookStore.Domain/BookStoreConsts.cs b/samples/BookStore/src/Acme.BookStore.Domain/BookStoreConsts.cs deleted file mode 100644 index e9d4c14540..0000000000 --- a/samples/BookStore/src/Acme.BookStore.Domain/BookStoreConsts.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Acme.BookStore -{ - public static class BookStoreConsts - { - public const string DbTablePrefix = "App"; - public const string DbSchema = null; - } -} diff --git a/samples/BookStore/src/Acme.BookStore.Domain/BookType.cs b/samples/BookStore/src/Acme.BookStore.Domain/BookType.cs new file mode 100644 index 0000000000..e5bf7e027c --- /dev/null +++ b/samples/BookStore/src/Acme.BookStore.Domain/BookType.cs @@ -0,0 +1,14 @@ +namespace Acme.BookStore +{ + public enum BookType : byte + { + Undefined, + Advanture, + Biography, + Fantastic, + Horror, + Science, + ScienceFiction, + Poetry + } +} \ No newline at end of file diff --git a/samples/BookStore/src/Acme.BookStore.EntityFrameworkCore/Acme.BookStore.EntityFrameworkCore.csproj b/samples/BookStore/src/Acme.BookStore.EntityFrameworkCore/Acme.BookStore.EntityFrameworkCore.csproj index f5521eed84..66b8fc5e35 100644 --- a/samples/BookStore/src/Acme.BookStore.EntityFrameworkCore/Acme.BookStore.EntityFrameworkCore.csproj +++ b/samples/BookStore/src/Acme.BookStore.EntityFrameworkCore/Acme.BookStore.EntityFrameworkCore.csproj @@ -5,6 +5,10 @@ Acme.BookStore + + + + diff --git a/samples/BookStore/src/Acme.BookStore.EntityFrameworkCore/EntityFrameworkCore/BookStoreDbContext.cs b/samples/BookStore/src/Acme.BookStore.EntityFrameworkCore/EntityFrameworkCore/BookStoreDbContext.cs index b99cb5bed1..86d276a6f7 100644 --- a/samples/BookStore/src/Acme.BookStore.EntityFrameworkCore/EntityFrameworkCore/BookStoreDbContext.cs +++ b/samples/BookStore/src/Acme.BookStore.EntityFrameworkCore/EntityFrameworkCore/BookStoreDbContext.cs @@ -8,6 +8,8 @@ namespace Acme.BookStore.EntityFrameworkCore { public class BookStoreDbContext : AbpDbContext { + public DbSet Book { get; set; } + public BookStoreDbContext(DbContextOptions options) : base(options) { @@ -21,13 +23,6 @@ namespace Acme.BookStore.EntityFrameworkCore modelBuilder.ConfigureIdentity(); modelBuilder.ConfigurePermissionManagement(); modelBuilder.ConfigureSettingManagement(); - - //builder.Entity(b => - //{ - // b.ToTable(BookStoreConsts.DbTablePrefix + "PermissionGrants", BookStoreConsts.DbSchema); - - // ... - //}); } } } diff --git a/samples/BookStore/src/Acme.BookStore.EntityFrameworkCore/Migrations/20180628193337_Created_Book_Entity.Designer.cs b/samples/BookStore/src/Acme.BookStore.EntityFrameworkCore/Migrations/20180628193337_Created_Book_Entity.Designer.cs new file mode 100644 index 0000000000..a06036b726 --- /dev/null +++ b/samples/BookStore/src/Acme.BookStore.EntityFrameworkCore/Migrations/20180628193337_Created_Book_Entity.Designer.cs @@ -0,0 +1,356 @@ +// +using System; +using Acme.BookStore.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Acme.BookStore.Migrations +{ + [DbContext(typeof(BookStoreDbContext))] + [Migration("20180628193337_Created_Book_Entity")] + partial class Created_Book_Entity + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.1-rtm-30846") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Acme.BookStore.Book", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128); + + b.Property("Price"); + + b.Property("PublishDate"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.ToTable("Books"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConcurrencyStamp"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(256); + + b.Property("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName"); + + b.ToTable("AbpRoles"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType") + .IsRequired() + .HasMaxLength(256); + + b.Property("ClaimValue") + .HasMaxLength(1024); + + b.Property("RoleId"); + + b.Property("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AbpRoleClaims"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessFailedCount") + .ValueGeneratedOnAdd() + .HasColumnName("AccessFailedCount") + .HasDefaultValue(0); + + b.Property("ConcurrencyStamp") + .IsRequired() + .HasColumnName("ConcurrencyStamp") + .HasMaxLength(256); + + b.Property("Email") + .HasColumnName("Email") + .HasMaxLength(256); + + b.Property("EmailConfirmed") + .ValueGeneratedOnAdd() + .HasColumnName("EmailConfirmed") + .HasDefaultValue(false); + + b.Property("ExtraProperties") + .HasColumnName("ExtraProperties"); + + b.Property("LockoutEnabled") + .ValueGeneratedOnAdd() + .HasColumnName("LockoutEnabled") + .HasDefaultValue(false); + + b.Property("LockoutEnd"); + + b.Property("NormalizedEmail") + .HasColumnName("NormalizedEmail") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .IsRequired() + .HasColumnName("NormalizedUserName") + .HasMaxLength(256); + + b.Property("PasswordHash") + .HasColumnName("PasswordHash") + .HasMaxLength(256); + + b.Property("PhoneNumber") + .HasColumnName("PhoneNumber") + .HasMaxLength(16); + + b.Property("PhoneNumberConfirmed") + .ValueGeneratedOnAdd() + .HasColumnName("PhoneNumberConfirmed") + .HasDefaultValue(false); + + b.Property("SecurityStamp") + .IsRequired() + .HasColumnName("SecurityStamp") + .HasMaxLength(256); + + b.Property("TenantId") + .HasColumnName("TenantId"); + + b.Property("TwoFactorEnabled") + .ValueGeneratedOnAdd() + .HasColumnName("TwoFactorEnabled") + .HasDefaultValue(false); + + b.Property("UserName") + .IsRequired() + .HasColumnName("UserName") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("NormalizedEmail"); + + b.HasIndex("NormalizedUserName"); + + b.HasIndex("UserName"); + + b.ToTable("AbpUsers"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ClaimType") + .IsRequired() + .HasMaxLength(256); + + b.Property("ClaimValue") + .HasMaxLength(1024); + + b.Property("TenantId"); + + b.Property("UserId"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AbpUserClaims"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserLogin", b => + { + b.Property("UserId"); + + b.Property("LoginProvider") + .HasMaxLength(64); + + b.Property("ProviderDisplayName") + .HasMaxLength(128); + + b.Property("ProviderKey") + .IsRequired() + .HasMaxLength(196); + + b.Property("TenantId"); + + b.HasKey("UserId", "LoginProvider"); + + b.HasIndex("LoginProvider", "ProviderKey"); + + b.ToTable("AbpUserLogins"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.Property("TenantId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId", "UserId"); + + b.ToTable("AbpUserRoles"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider") + .HasMaxLength(128); + + b.Property("Name"); + + b.Property("TenantId"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AbpUserTokens"); + }); + + modelBuilder.Entity("Volo.Abp.PermissionManagement.PermissionGrant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128); + + b.Property("ProviderKey") + .IsRequired() + .HasMaxLength(64); + + b.Property("ProviderName") + .IsRequired() + .HasMaxLength(64); + + b.Property("TenantId"); + + b.HasKey("Id"); + + b.HasIndex("Name", "ProviderName", "ProviderKey"); + + b.ToTable("AbpPermissionGrants"); + }); + + modelBuilder.Entity("Volo.Abp.SettingManagement.Setting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128); + + b.Property("ProviderKey") + .HasMaxLength(64); + + b.Property("ProviderName") + .HasMaxLength(64); + + b.Property("Value") + .IsRequired() + .HasMaxLength(2048); + + b.HasKey("Id"); + + b.HasIndex("Name", "ProviderName", "ProviderKey"); + + b.ToTable("AbpSettings"); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityRoleClaim", b => + { + b.HasOne("Volo.Abp.Identity.IdentityRole") + .WithMany("Claims") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserClaim", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser") + .WithMany("Claims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserLogin", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser") + .WithMany("Logins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserRole", b => + { + b.HasOne("Volo.Abp.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Volo.Abp.Identity.IdentityUser") + .WithMany("Roles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Volo.Abp.Identity.IdentityUserToken", b => + { + b.HasOne("Volo.Abp.Identity.IdentityUser") + .WithMany("Tokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/samples/BookStore/src/Acme.BookStore.EntityFrameworkCore/Migrations/20180628193337_Created_Book_Entity.cs b/samples/BookStore/src/Acme.BookStore.EntityFrameworkCore/Migrations/20180628193337_Created_Book_Entity.cs new file mode 100644 index 0000000000..1e6d5ec0ed --- /dev/null +++ b/samples/BookStore/src/Acme.BookStore.EntityFrameworkCore/Migrations/20180628193337_Created_Book_Entity.cs @@ -0,0 +1,32 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Acme.BookStore.Migrations +{ + public partial class Created_Book_Entity : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Books", + columns: table => new + { + Id = table.Column(nullable: false), + Name = table.Column(maxLength: 128, nullable: false), + Type = table.Column(nullable: false), + PublishDate = table.Column(nullable: false), + Price = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Books", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Books"); + } + } +} diff --git a/samples/BookStore/src/Acme.BookStore.EntityFrameworkCore/Migrations/BookStoreDbContextModelSnapshot.cs b/samples/BookStore/src/Acme.BookStore.EntityFrameworkCore/Migrations/BookStoreDbContextModelSnapshot.cs index 4321228aff..d1693bb99b 100644 --- a/samples/BookStore/src/Acme.BookStore.EntityFrameworkCore/Migrations/BookStoreDbContextModelSnapshot.cs +++ b/samples/BookStore/src/Acme.BookStore.EntityFrameworkCore/Migrations/BookStoreDbContextModelSnapshot.cs @@ -1,13 +1,10 @@ // using System; -using System.Collections.Generic; +using Acme.BookStore.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Metadata.Internal; -using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Acme.BookStore.EntityFrameworkCore; namespace Acme.BookStore.Migrations { @@ -18,9 +15,30 @@ namespace Acme.BookStore.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "2.1.0-preview2-30571") + .HasAnnotation("ProductVersion", "2.1.1-rtm-30846") + .HasAnnotation("Relational:MaxIdentifierLength", 128) .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + modelBuilder.Entity("Acme.BookStore.Book", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128); + + b.Property("Price"); + + b.Property("PublishDate"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.ToTable("Books"); + }); + modelBuilder.Entity("Volo.Abp.Identity.IdentityRole", b => { b.Property("Id")