Browse Source

Merge pull request #15303 from abpframework/upradge-bookstore-blazor-ef-abp-7-0

Upradge bookstore blazor and angular
pull/15377/head
Halil İbrahim Kalkan 4 years ago
committed by GitHub
parent
commit
11ecd2f103
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 258
      docs/en/Tutorials/Part-1.md
  2. 814
      docs/en/Tutorials/Part-10.md
  3. 17
      docs/en/Tutorials/Part-2.md
  4. 97
      docs/en/Tutorials/Part-3.md
  5. 144
      docs/en/Tutorials/Part-4.md
  6. 87
      docs/en/Tutorials/Part-5.md
  7. 195
      docs/en/Tutorials/Part-6.md
  8. 132
      docs/en/Tutorials/Part-7.md
  9. 379
      docs/en/Tutorials/Part-8.md
  10. 416
      docs/en/Tutorials/Part-9.md
  11. BIN
      docs/en/Tutorials/images/blazor-add-book-button-2.png
  12. BIN
      docs/en/Tutorials/images/blazor-bookstore-book-list-2.png
  13. BIN
      docs/en/Tutorials/images/blazor-bookstore-book-list-with-authors-2.png
  14. BIN
      docs/en/Tutorials/images/blazor-delete-book-action-2.png
  15. BIN
      docs/en/Tutorials/images/blazor-edit-book-action-3.png
  16. BIN
      docs/en/Tutorials/images/blazor-edit-book-modal-2.png
  17. BIN
      docs/en/Tutorials/images/blazor-new-book-modal-2.png
  18. BIN
      docs/en/Tutorials/images/book-create-modal-with-author-2.png
  19. BIN
      docs/en/Tutorials/images/bookstore-actions-buttons-2.png
  20. BIN
      docs/en/Tutorials/images/bookstore-angular-author-selection-2.png
  21. BIN
      docs/en/Tutorials/images/bookstore-angular-authors-page-2.png
  22. BIN
      docs/en/Tutorials/images/bookstore-author-permissions-3.png
  23. BIN
      docs/en/Tutorials/images/bookstore-authors-page-3.png
  24. BIN
      docs/en/Tutorials/images/bookstore-book-list-angular.png
  25. BIN
      docs/en/Tutorials/images/bookstore-books-with-authorname-angular-2.png
  26. BIN
      docs/en/Tutorials/images/bookstore-confirmation-popup-2.png
  27. BIN
      docs/en/Tutorials/images/bookstore-empty-new-book-modal-2.png
  28. BIN
      docs/en/Tutorials/images/bookstore-final-actions-dropdown-2.png
  29. BIN
      docs/en/Tutorials/images/bookstore-new-book-form-v3.png

258
docs/en/Tutorials/Part-1.md

@ -63,18 +63,17 @@ The main entity of the application is the `Book`. Create a `Books` folder (names
using System; using System;
using Volo.Abp.Domain.Entities.Auditing; using Volo.Abp.Domain.Entities.Auditing;
namespace Acme.BookStore.Books namespace Acme.BookStore.Books;
public class Book : AuditedAggregateRoot<Guid>
{ {
public class Book : AuditedAggregateRoot<Guid> public string Name { get; set; }
{
public string Name { get; set; }
public BookType Type { get; set; } public BookType Type { get; set; }
public DateTime PublishDate { get; set; } public DateTime PublishDate { get; set; }
public float Price { get; set; } public float Price { get; set; }
}
} }
```` ````
@ -89,20 +88,19 @@ namespace Acme.BookStore.Books
The `Book` entity uses the `BookType` enum. Create a `Books` folder (namespace) in the `Acme.BookStore.Domain.Shared` project and add a `BookType` inside it: The `Book` entity uses the `BookType` enum. Create a `Books` folder (namespace) in the `Acme.BookStore.Domain.Shared` project and add a `BookType` inside it:
````csharp ````csharp
namespace Acme.BookStore.Books namespace Acme.BookStore.Books;
public enum BookType
{ {
public enum BookType Undefined,
{ Adventure,
Undefined, Biography,
Adventure, Dystopia,
Biography, Fantastic,
Dystopia, Horror,
Fantastic, Science,
Horror, ScienceFiction,
Science, Poetry
ScienceFiction,
Poetry
}
} }
```` ````
@ -150,34 +148,33 @@ Navigate to the `OnModelCreating` method in the `BookStoreDbContext` class and a
using Acme.BookStore.Books; using Acme.BookStore.Books;
... ...
namespace Acme.BookStore.EntityFrameworkCore namespace Acme.BookStore.EntityFrameworkCore;
public class BookStoreDbContext :
AbpDbContext<BookStoreDbContext>,
IIdentityDbContext,
ITenantManagementDbContext
{ {
public class BookStoreDbContext : ...
AbpDbContext<BookStoreDbContext>,
IIdentityDbContext,
ITenantManagementDbContext
{
...
protected override void OnModelCreating(ModelBuilder builder) protected override void OnModelCreating(ModelBuilder builder)
{ {
base.OnModelCreating(builder); base.OnModelCreating(builder);
/* Include modules to your migration db context */ /* Include modules to your migration db context */
builder.ConfigurePermissionManagement(); builder.ConfigurePermissionManagement();
... ...
/* Configure your own tables/entities inside here */ /* Configure your own tables/entities inside here */
builder.Entity<Book>(b => builder.Entity<Book>(b =>
{ {
b.ToTable(BookStoreConsts.DbTablePrefix + "Books", b.ToTable(BookStoreConsts.DbTablePrefix + "Books",
BookStoreConsts.DbSchema); BookStoreConsts.DbSchema);
b.ConfigureByConvention(); //auto configure for the base class props b.ConfigureByConvention(); //auto configure for the base class props
b.Property(x => x.Name).IsRequired().HasMaxLength(128); b.Property(x => x.Name).IsRequired().HasMaxLength(128);
}); });
}
} }
} }
```` ````
@ -217,44 +214,43 @@ using Volo.Abp.Data;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore namespace Acme.BookStore;
public class BookStoreDataSeederContributor
: IDataSeedContributor, ITransientDependency
{ {
public class BookStoreDataSeederContributor private readonly IRepository<Book, Guid> _bookRepository;
: IDataSeedContributor, ITransientDependency
{
private readonly IRepository<Book, Guid> _bookRepository;
public BookStoreDataSeederContributor(IRepository<Book, Guid> bookRepository) public BookStoreDataSeederContributor(IRepository<Book, Guid> bookRepository)
{ {
_bookRepository = bookRepository; _bookRepository = bookRepository;
} }
public async Task SeedAsync(DataSeedContext context) public async Task SeedAsync(DataSeedContext context)
{
if (await _bookRepository.GetCountAsync() <= 0)
{ {
if (await _bookRepository.GetCountAsync() <= 0) await _bookRepository.InsertAsync(
{ new Book
await _bookRepository.InsertAsync( {
new Book Name = "1984",
{ Type = BookType.Dystopia,
Name = "1984", PublishDate = new DateTime(1949, 6, 8),
Type = BookType.Dystopia, Price = 19.84f
PublishDate = new DateTime(1949, 6, 8), },
Price = 19.84f autoSave: true
}, );
autoSave: true
); await _bookRepository.InsertAsync(
new Book
await _bookRepository.InsertAsync( {
new Book Name = "The Hitchhiker's Guide to the Galaxy",
{ Type = BookType.ScienceFiction,
Name = "The Hitchhiker's Guide to the Galaxy", PublishDate = new DateTime(1995, 9, 27),
Type = BookType.ScienceFiction, Price = 42.0f
PublishDate = new DateTime(1995, 9, 27), },
Price = 42.0f autoSave: true
}, );
autoSave: true
);
}
} }
} }
} }
@ -287,18 +283,17 @@ In this section, you will create an application service to get, create, update a
using System; using System;
using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Dtos;
namespace Acme.BookStore.Books namespace Acme.BookStore.Books;
public class BookDto : AuditedEntityDto<Guid>
{ {
public class BookDto : AuditedEntityDto<Guid> public string Name { get; set; }
{
public string Name { get; set; }
public BookType Type { get; set; } public BookType Type { get; set; }
public DateTime PublishDate { get; set; } public DateTime PublishDate { get; set; }
public float Price { get; set; } public float Price { get; set; }
}
} }
```` ````
@ -312,14 +307,13 @@ It will be needed to map the `Book` entities to the `BookDto` objects while retu
using Acme.BookStore.Books; using Acme.BookStore.Books;
using AutoMapper; using AutoMapper;
namespace Acme.BookStore namespace Acme.BookStore;
public class BookStoreApplicationAutoMapperProfile : Profile
{ {
public class BookStoreApplicationAutoMapperProfile : Profile public BookStoreApplicationAutoMapperProfile()
{ {
public BookStoreApplicationAutoMapperProfile() CreateMap<Book, BookDto>();
{
CreateMap<Book, BookDto>();
}
} }
} }
```` ````
@ -334,24 +328,23 @@ Create a `CreateUpdateBookDto` class in the `Books` folder (namespace) of the `A
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace Acme.BookStore.Books namespace Acme.BookStore.Books;
public class CreateUpdateBookDto
{ {
public class CreateUpdateBookDto [Required]
{ [StringLength(128)]
[Required] public string Name { get; set; }
[StringLength(128)]
public string Name { get; set; }
[Required] [Required]
public BookType Type { get; set; } = BookType.Undefined; public BookType Type { get; set; } = BookType.Undefined;
[Required] [Required]
[DataType(DataType.Date)] [DataType(DataType.Date)]
public DateTime PublishDate { get; set; } = DateTime.Now; public DateTime PublishDate { get; set; } = DateTime.Now;
[Required] [Required]
public float Price { get; set; } public float Price { get; set; }
}
} }
```` ````
@ -364,15 +357,14 @@ As done to the `BookDto` above, we should define the mapping from the `CreateUpd
using Acme.BookStore.Books; using Acme.BookStore.Books;
using AutoMapper; using AutoMapper;
namespace Acme.BookStore namespace Acme.BookStore;
public class BookStoreApplicationAutoMapperProfile : Profile
{ {
public class BookStoreApplicationAutoMapperProfile : Profile public BookStoreApplicationAutoMapperProfile()
{ {
public BookStoreApplicationAutoMapperProfile() CreateMap<Book, BookDto>();
{ CreateMap<CreateUpdateBookDto, Book>();
CreateMap<Book, BookDto>();
CreateMap<CreateUpdateBookDto, Book>();
}
} }
} }
```` ````
@ -386,17 +378,16 @@ using System;
using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services; using Volo.Abp.Application.Services;
namespace Acme.BookStore.Books namespace Acme.BookStore.Books;
public interface IBookAppService :
ICrudAppService< //Defines CRUD methods
BookDto, //Used to show books
Guid, //Primary key of the book entity
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateBookDto> //Used to create/update a book
{ {
public interface IBookAppService :
ICrudAppService< //Defines CRUD methods
BookDto, //Used to show books
Guid, //Primary key of the book entity
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateBookDto> //Used to create/update a book
{
}
} }
```` ````
@ -414,22 +405,21 @@ using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services; using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore.Books namespace Acme.BookStore.Books;
public class BookAppService :
CrudAppService<
Book, //The Book entity
BookDto, //Used to show books
Guid, //Primary key of the book entity
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateBookDto>, //Used to create/update a book
IBookAppService //implement the IBookAppService
{ {
public class BookAppService : public BookAppService(IRepository<Book, Guid> repository)
CrudAppService< : base(repository)
Book, //The Book entity
BookDto, //Used to show books
Guid, //Primary key of the book entity
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateBookDto>, //Used to create/update a book
IBookAppService //implement the IBookAppService
{ {
public BookAppService(IRepository<Book, Guid> repository)
: base(repository)
{
}
} }
} }
```` ````

814
docs/en/Tutorials/Part-10.md

@ -141,72 +141,71 @@ using Volo.Abp.Data;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore namespace Acme.BookStore;
public class BookStoreDataSeederContributor
: IDataSeedContributor, ITransientDependency
{ {
public class BookStoreDataSeederContributor private readonly IRepository<Book, Guid> _bookRepository;
: IDataSeedContributor, ITransientDependency private readonly IAuthorRepository _authorRepository;
private readonly AuthorManager _authorManager;
public BookStoreDataSeederContributor(
IRepository<Book, Guid> bookRepository,
IAuthorRepository authorRepository,
AuthorManager authorManager)
{
_bookRepository = bookRepository;
_authorRepository = authorRepository;
_authorManager = authorManager;
}
public async Task SeedAsync(DataSeedContext context)
{ {
private readonly IRepository<Book, Guid> _bookRepository; if (await _bookRepository.GetCountAsync() > 0)
private readonly IAuthorRepository _authorRepository;
private readonly AuthorManager _authorManager;
public BookStoreDataSeederContributor(
IRepository<Book, Guid> bookRepository,
IAuthorRepository authorRepository,
AuthorManager authorManager)
{ {
_bookRepository = bookRepository; return;
_authorRepository = authorRepository;
_authorManager = authorManager;
} }
public async Task SeedAsync(DataSeedContext context) var orwell = await _authorRepository.InsertAsync(
{ await _authorManager.CreateAsync(
if (await _bookRepository.GetCountAsync() > 0) "George Orwell",
new DateTime(1903, 06, 25),
"Orwell produced literary criticism and poetry, fiction and polemical journalism; and is best known for the allegorical novella Animal Farm (1945) and the dystopian novel Nineteen Eighty-Four (1949)."
)
);
var douglas = await _authorRepository.InsertAsync(
await _authorManager.CreateAsync(
"Douglas Adams",
new DateTime(1952, 03, 11),
"Douglas Adams was an English author, screenwriter, essayist, humorist, satirist and dramatist. Adams was an advocate for environmentalism and conservation, a lover of fast cars, technological innovation and the Apple Macintosh, and a self-proclaimed 'radical atheist'."
)
);
await _bookRepository.InsertAsync(
new Book
{ {
return; AuthorId = orwell.Id, // SET THE AUTHOR
} Name = "1984",
Type = BookType.Dystopia,
var orwell = await _authorRepository.InsertAsync( PublishDate = new DateTime(1949, 6, 8),
await _authorManager.CreateAsync( Price = 19.84f
"George Orwell", },
new DateTime(1903, 06, 25), autoSave: true
"Orwell produced literary criticism and poetry, fiction and polemical journalism; and is best known for the allegorical novella Animal Farm (1945) and the dystopian novel Nineteen Eighty-Four (1949)." );
)
); await _bookRepository.InsertAsync(
new Book
var douglas = await _authorRepository.InsertAsync( {
await _authorManager.CreateAsync( AuthorId = douglas.Id, // SET THE AUTHOR
"Douglas Adams", Name = "The Hitchhiker's Guide to the Galaxy",
new DateTime(1952, 03, 11), Type = BookType.ScienceFiction,
"Douglas Adams was an English author, screenwriter, essayist, humorist, satirist and dramatist. Adams was an advocate for environmentalism and conservation, a lover of fast cars, technological innovation and the Apple Macintosh, and a self-proclaimed 'radical atheist'." PublishDate = new DateTime(1995, 9, 27),
) Price = 42.0f
); },
autoSave: true
await _bookRepository.InsertAsync( );
new Book
{
AuthorId = orwell.Id, // SET THE AUTHOR
Name = "1984",
Type = BookType.Dystopia,
PublishDate = new DateTime(1949, 6, 8),
Price = 19.84f
},
autoSave: true
);
await _bookRepository.InsertAsync(
new Book
{
AuthorId = douglas.Id, // SET THE AUTHOR
Name = "The Hitchhiker's Guide to the Galaxy",
Type = BookType.ScienceFiction,
PublishDate = new DateTime(1995, 9, 27),
Price = 42.0f
},
autoSave: true
);
}
} }
} }
```` ````
@ -248,22 +247,21 @@ The final `BookDto` class should be following:
using System; using System;
using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Dtos;
namespace Acme.BookStore.Books namespace Acme.BookStore.Books;
public class BookDto : AuditedEntityDto<Guid>
{ {
public class BookDto : AuditedEntityDto<Guid> public Guid AuthorId { get; set; }
{
public Guid AuthorId { get; set; }
public string AuthorName { get; set; } public string AuthorName { get; set; }
public string Name { get; set; } public string Name { get; set; }
public BookType Type { get; set; } public BookType Type { get; set; }
public DateTime PublishDate { get; set; } public DateTime PublishDate { get; set; }
public float Price { get; set; } public float Price { get; set; }
}
} }
``` ```
@ -283,12 +281,11 @@ Create a new class, `AuthorLookupDto`, inside the `Books` folder of the `Acme.Bo
using System; using System;
using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Dtos;
namespace Acme.BookStore.Books namespace Acme.BookStore.Books;
public class AuthorLookupDto : EntityDto<Guid>
{ {
public class AuthorLookupDto : EntityDto<Guid> public string Name { get; set; }
{
public string Name { get; set; }
}
} }
```` ````
@ -304,18 +301,17 @@ using System.Threading.Tasks;
using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services; using Volo.Abp.Application.Services;
namespace Acme.BookStore.Books namespace Acme.BookStore.Books;
public interface IBookAppService :
ICrudAppService< //Defines CRUD methods
BookDto, //Used to show books
Guid, //Primary key of the book entity
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateBookDto> //Used to create/update a book
{ {
public interface IBookAppService : // ADD the NEW METHOD
ICrudAppService< //Defines CRUD methods Task<ListResultDto<AuthorLookupDto>> GetAuthorLookupAsync();
BookDto, //Used to show books
Guid, //Primary key of the book entity
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateBookDto> //Used to create/update a book
{
// ADD the NEW METHOD
Task<ListResultDto<AuthorLookupDto>> GetAuthorLookupAsync();
}
} }
```` ````
@ -341,119 +337,118 @@ using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore.Books namespace Acme.BookStore.Books;
[Authorize(BookStorePermissions.Books.Default)]
public class BookAppService :
CrudAppService<
Book, //The Book entity
BookDto, //Used to show books
Guid, //Primary key of the book entity
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateBookDto>, //Used to create/update a book
IBookAppService //implement the IBookAppService
{ {
[Authorize(BookStorePermissions.Books.Default)] private readonly IAuthorRepository _authorRepository;
public class BookAppService :
CrudAppService< public BookAppService(
Book, //The Book entity IRepository<Book, Guid> repository,
BookDto, //Used to show books IAuthorRepository authorRepository)
Guid, //Primary key of the book entity : base(repository)
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateBookDto>, //Used to create/update a book
IBookAppService //implement the IBookAppService
{ {
private readonly IAuthorRepository _authorRepository; _authorRepository = authorRepository;
GetPolicyName = BookStorePermissions.Books.Default;
GetListPolicyName = BookStorePermissions.Books.Default;
CreatePolicyName = BookStorePermissions.Books.Create;
UpdatePolicyName = BookStorePermissions.Books.Edit;
DeletePolicyName = BookStorePermissions.Books.Create;
}
public BookAppService( public override async Task<BookDto> GetAsync(Guid id)
IRepository<Book, Guid> repository, {
IAuthorRepository authorRepository) //Get the IQueryable<Book> from the repository
: base(repository) var queryable = await Repository.GetQueryableAsync();
//Prepare a query to join books and authors
var query = from book in queryable
join author in await _authorRepository.GetQueryableAsync() on book.AuthorId equals author.Id
where book.Id == id
select new { book, author };
//Execute the query and get the book with author
var queryResult = await AsyncExecuter.FirstOrDefaultAsync(query);
if (queryResult == null)
{ {
_authorRepository = authorRepository; throw new EntityNotFoundException(typeof(Book), id);
GetPolicyName = BookStorePermissions.Books.Default;
GetListPolicyName = BookStorePermissions.Books.Default;
CreatePolicyName = BookStorePermissions.Books.Create;
UpdatePolicyName = BookStorePermissions.Books.Edit;
DeletePolicyName = BookStorePermissions.Books.Create;
} }
public override async Task<BookDto> GetAsync(Guid id) var bookDto = ObjectMapper.Map<Book, BookDto>(queryResult.book);
{ bookDto.AuthorName = queryResult.author.Name;
//Get the IQueryable<Book> from the repository return bookDto;
var queryable = await Repository.GetQueryableAsync(); }
//Prepare a query to join books and authors
var query = from book in queryable
join author in await _authorRepository.GetQueryableAsync() on book.AuthorId equals author.Id
where book.Id == id
select new { book, author };
//Execute the query and get the book with author
var queryResult = await AsyncExecuter.FirstOrDefaultAsync(query);
if (queryResult == null)
{
throw new EntityNotFoundException(typeof(Book), id);
}
var bookDto = ObjectMapper.Map<Book, BookDto>(queryResult.book); public override async Task<PagedResultDto<BookDto>> GetListAsync(PagedAndSortedResultRequestDto input)
bookDto.AuthorName = queryResult.author.Name; {
return bookDto; //Get the IQueryable<Book> from the repository
} var queryable = await Repository.GetQueryableAsync();
public override async Task<PagedResultDto<BookDto>> GetListAsync(PagedAndSortedResultRequestDto input) //Prepare a query to join books and authors
{ var query = from book in queryable
//Get the IQueryable<Book> from the repository join author in await _authorRepository.GetQueryableAsync() on book.AuthorId equals author.Id
var queryable = await Repository.GetQueryableAsync(); select new {book, author};
//Prepare a query to join books and authors //Paging
var query = from book in queryable query = query
join author in await _authorRepository.GetQueryableAsync() on book.AuthorId equals author.Id .OrderBy(NormalizeSorting(input.Sorting))
select new {book, author}; .Skip(input.SkipCount)
.Take(input.MaxResultCount);
//Paging //Execute the query and get a list
query = query var queryResult = await AsyncExecuter.ToListAsync(query);
.OrderBy(NormalizeSorting(input.Sorting))
.Skip(input.SkipCount)
.Take(input.MaxResultCount);
//Execute the query and get a list //Convert the query result to a list of BookDto objects
var queryResult = await AsyncExecuter.ToListAsync(query); var bookDtos = queryResult.Select(x =>
{
var bookDto = ObjectMapper.Map<Book, BookDto>(x.book);
bookDto.AuthorName = x.author.Name;
return bookDto;
}).ToList();
//Convert the query result to a list of BookDto objects //Get the total count with another query
var bookDtos = queryResult.Select(x => var totalCount = await Repository.GetCountAsync();
{
var bookDto = ObjectMapper.Map<Book, BookDto>(x.book);
bookDto.AuthorName = x.author.Name;
return bookDto;
}).ToList();
//Get the total count with another query return new PagedResultDto<BookDto>(
var totalCount = await Repository.GetCountAsync(); totalCount,
bookDtos
);
}
return new PagedResultDto<BookDto>( public async Task<ListResultDto<AuthorLookupDto>> GetAuthorLookupAsync()
totalCount, {
bookDtos var authors = await _authorRepository.GetListAsync();
);
}
public async Task<ListResultDto<AuthorLookupDto>> GetAuthorLookupAsync() return new ListResultDto<AuthorLookupDto>(
{ ObjectMapper.Map<List<Author>, List<AuthorLookupDto>>(authors)
var authors = await _authorRepository.GetListAsync(); );
}
return new ListResultDto<AuthorLookupDto>( private static string NormalizeSorting(string sorting)
ObjectMapper.Map<List<Author>, List<AuthorLookupDto>>(authors) {
); if (sorting.IsNullOrEmpty())
{
return $"book.{nameof(Book.Name)}";
} }
private static string NormalizeSorting(string sorting) if (sorting.Contains("authorName", StringComparison.OrdinalIgnoreCase))
{ {
if (sorting.IsNullOrEmpty()) return sorting.Replace(
{ "authorName",
return $"book.{nameof(Book.Name)}"; "author.Name",
} StringComparison.OrdinalIgnoreCase
);
if (sorting.Contains("authorName", StringComparison.OrdinalIgnoreCase))
{
return sorting.Replace(
"authorName",
"author.Name",
StringComparison.OrdinalIgnoreCase
);
}
return $"book.{sorting}";
} }
return $"book.{sorting}";
} }
} }
``` ```
@ -485,108 +480,107 @@ using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services; using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore.Books namespace Acme.BookStore.Books;
[Authorize(BookStorePermissions.Books.Default)]
public class BookAppService :
CrudAppService<
Book, //The Book entity
BookDto, //Used to show books
Guid, //Primary key of the book entity
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateBookDto>, //Used to create/update a book
IBookAppService //implement the IBookAppService
{ {
[Authorize(BookStorePermissions.Books.Default)] private readonly IAuthorRepository _authorRepository;
public class BookAppService :
CrudAppService<
Book, //The Book entity
BookDto, //Used to show books
Guid, //Primary key of the book entity
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateBookDto>, //Used to create/update a book
IBookAppService //implement the IBookAppService
{
private readonly IAuthorRepository _authorRepository;
public BookAppService( public BookAppService(
IRepository<Book, Guid> repository, IRepository<Book, Guid> repository,
IAuthorRepository authorRepository) IAuthorRepository authorRepository)
: base(repository) : base(repository)
{ {
_authorRepository = authorRepository; _authorRepository = authorRepository;
GetPolicyName = BookStorePermissions.Books.Default; GetPolicyName = BookStorePermissions.Books.Default;
GetListPolicyName = BookStorePermissions.Books.Default; GetListPolicyName = BookStorePermissions.Books.Default;
CreatePolicyName = BookStorePermissions.Books.Create; CreatePolicyName = BookStorePermissions.Books.Create;
UpdatePolicyName = BookStorePermissions.Books.Edit; UpdatePolicyName = BookStorePermissions.Books.Edit;
DeletePolicyName = BookStorePermissions.Books.Create; DeletePolicyName = BookStorePermissions.Books.Create;
} }
public async override Task<BookDto> GetAsync(Guid id) public async override Task<BookDto> GetAsync(Guid id)
{ {
var book = await Repository.GetAsync(id); var book = await Repository.GetAsync(id);
var bookDto = ObjectMapper.Map<Book, BookDto>(book); var bookDto = ObjectMapper.Map<Book, BookDto>(book);
var author = await _authorRepository.GetAsync(book.AuthorId); var author = await _authorRepository.GetAsync(book.AuthorId);
bookDto.AuthorName = author.Name; bookDto.AuthorName = author.Name;
return bookDto; return bookDto;
} }
public async override Task<PagedResultDto<BookDto>> public async override Task<PagedResultDto<BookDto>>
GetListAsync(PagedAndSortedResultRequestDto input) GetListAsync(PagedAndSortedResultRequestDto input)
{
//Set a default sorting, if not provided
if (input.Sorting.IsNullOrWhiteSpace())
{ {
//Set a default sorting, if not provided input.Sorting = nameof(Book.Name);
if (input.Sorting.IsNullOrWhiteSpace()) }
{
input.Sorting = nameof(Book.Name);
}
//Get the IQueryable<Book> from the repository //Get the IQueryable<Book> from the repository
var queryable = await Repository.GetQueryableAsync(); var queryable = await Repository.GetQueryableAsync();
//Get the books //Get the books
var books = await AsyncExecuter.ToListAsync( var books = await AsyncExecuter.ToListAsync(
queryable queryable
.OrderBy(input.Sorting) .OrderBy(input.Sorting)
.Skip(input.SkipCount) .Skip(input.SkipCount)
.Take(input.MaxResultCount) .Take(input.MaxResultCount)
); );
//Convert to DTOs //Convert to DTOs
var bookDtos = ObjectMapper.Map<List<Book>, List<BookDto>>(books); var bookDtos = ObjectMapper.Map<List<Book>, List<BookDto>>(books);
//Get a lookup dictionary for the related authors //Get a lookup dictionary for the related authors
var authorDictionary = await GetAuthorDictionaryAsync(books); var authorDictionary = await GetAuthorDictionaryAsync(books);
//Set AuthorName for the DTOs //Set AuthorName for the DTOs
bookDtos.ForEach(bookDto => bookDto.AuthorName = bookDtos.ForEach(bookDto => bookDto.AuthorName =
authorDictionary[bookDto.AuthorId].Name); authorDictionary[bookDto.AuthorId].Name);
//Get the total count with another query (required for the paging) //Get the total count with another query (required for the paging)
var totalCount = await Repository.GetCountAsync(); var totalCount = await Repository.GetCountAsync();
return new PagedResultDto<BookDto>( return new PagedResultDto<BookDto>(
totalCount, totalCount,
bookDtos bookDtos
); );
} }
public async Task<ListResultDto<AuthorLookupDto>> GetAuthorLookupAsync() public async Task<ListResultDto<AuthorLookupDto>> GetAuthorLookupAsync()
{ {
var authors = await _authorRepository.GetListAsync(); var authors = await _authorRepository.GetListAsync();
return new ListResultDto<AuthorLookupDto>( return new ListResultDto<AuthorLookupDto>(
ObjectMapper.Map<List<Author>, List<AuthorLookupDto>>(authors) ObjectMapper.Map<List<Author>, List<AuthorLookupDto>>(authors)
); );
} }
private async Task<Dictionary<Guid, Author>> private async Task<Dictionary<Guid, Author>>
GetAuthorDictionaryAsync(List<Book> books) GetAuthorDictionaryAsync(List<Book> books)
{ {
var authorIds = books var authorIds = books
.Select(b => b.AuthorId) .Select(b => b.AuthorId)
.Distinct() .Distinct()
.ToArray(); .ToArray();
var queryable = await _authorRepository.GetQueryableAsync(); var queryable = await _authorRepository.GetQueryableAsync();
var authors = await AsyncExecuter.ToListAsync( var authors = await AsyncExecuter.ToListAsync(
queryable.Where(a => authorIds.Contains(a.Id)) queryable.Where(a => authorIds.Contains(a.Id))
); );
return authors.ToDictionary(x => x.Id, x => x); return authors.ToDictionary(x => x.Id, x => x);
}
} }
} }
``` ```
@ -623,76 +617,76 @@ using Volo.Abp.Application.Dtos;
using Volo.Abp.Validation; using Volo.Abp.Validation;
using Xunit; using Xunit;
namespace Acme.BookStore.Books namespace Acme.BookStore.Books;
{ {{if DB=="Mongo"}}
[Collection(BookStoreTestConsts.CollectionDefinitionName)]{{end}} {{if DB=="Mongo"}}
public class BookAppService_Tests : BookStoreApplicationTestBase [Collection(BookStoreTestConsts.CollectionDefinitionName)]{{end}}
public class BookAppService_Tests : BookStoreApplicationTestBase
{
private readonly IBookAppService _bookAppService;
private readonly IAuthorAppService _authorAppService;
public BookAppService_Tests()
{ {
private readonly IBookAppService _bookAppService; _bookAppService = GetRequiredService<IBookAppService>();
private readonly IAuthorAppService _authorAppService; _authorAppService = GetRequiredService<IAuthorAppService>();
}
public BookAppService_Tests() [Fact]
{ public async Task Should_Get_List_Of_Books()
_bookAppService = GetRequiredService<IBookAppService>(); {
_authorAppService = GetRequiredService<IAuthorAppService>(); //Act
} var result = await _bookAppService.GetListAsync(
new PagedAndSortedResultRequestDto()
);
//Assert
result.TotalCount.ShouldBeGreaterThan(0);
result.Items.ShouldContain(b => b.Name == "1984" &&
b.AuthorName == "George Orwell");
}
[Fact] [Fact]
public async Task Should_Get_List_Of_Books() public async Task Should_Create_A_Valid_Book()
{ {
//Act var authors = await _authorAppService.GetListAsync(new GetAuthorListDto());
var result = await _bookAppService.GetListAsync( var firstAuthor = authors.Items.First();
new PagedAndSortedResultRequestDto()
);
//Assert //Act
result.TotalCount.ShouldBeGreaterThan(0); var result = await _bookAppService.CreateAsync(
result.Items.ShouldContain(b => b.Name == "1984" && new CreateUpdateBookDto
b.AuthorName == "George Orwell"); {
} AuthorId = firstAuthor.Id,
Name = "New test book 42",
Price = 10,
PublishDate = System.DateTime.Now,
Type = BookType.ScienceFiction
}
);
[Fact] //Assert
public async Task Should_Create_A_Valid_Book() result.Id.ShouldNotBe(Guid.Empty);
{ result.Name.ShouldBe("New test book 42");
var authors = await _authorAppService.GetListAsync(new GetAuthorListDto()); }
var firstAuthor = authors.Items.First();
//Act [Fact]
var result = await _bookAppService.CreateAsync( public async Task Should_Not_Create_A_Book_Without_Name()
{
var exception = await Assert.ThrowsAsync<AbpValidationException>(async () =>
{
await _bookAppService.CreateAsync(
new CreateUpdateBookDto new CreateUpdateBookDto
{ {
AuthorId = firstAuthor.Id, Name = "",
Name = "New test book 42",
Price = 10, Price = 10,
PublishDate = System.DateTime.Now, PublishDate = DateTime.Now,
Type = BookType.ScienceFiction Type = BookType.ScienceFiction
} }
); );
});
//Assert exception.ValidationErrors
result.Id.ShouldNotBe(Guid.Empty); .ShouldContain(err => err.MemberNames.Any(m => m == "Name"));
result.Name.ShouldBe("New test book 42");
}
[Fact]
public async Task Should_Not_Create_A_Book_Without_Name()
{
var exception = await Assert.ThrowsAsync<AbpValidationException>(async () =>
{
await _bookAppService.CreateAsync(
new CreateUpdateBookDto
{
Name = "",
Price = 10,
PublishDate = DateTime.Now,
Type = BookType.ScienceFiction
}
);
});
exception.ValidationErrors
.ShouldContain(err => err.MemberNames.Any(m => m == "Name"));
}
} }
} }
``` ```
@ -751,61 +745,60 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form;
namespace Acme.BookStore.Web.Pages.Books namespace Acme.BookStore.Web.Pages.Books;
public class CreateModalModel : BookStorePageModel
{ {
public class CreateModalModel : BookStorePageModel [BindProperty]
{ public CreateBookViewModel Book { get; set; }
[BindProperty]
public CreateBookViewModel Book { get; set; }
public List<SelectListItem> Authors { get; set; } public List<SelectListItem> Authors { get; set; }
private readonly IBookAppService _bookAppService; private readonly IBookAppService _bookAppService;
public CreateModalModel( public CreateModalModel(
IBookAppService bookAppService) IBookAppService bookAppService)
{ {
_bookAppService = bookAppService; _bookAppService = bookAppService;
} }
public async Task OnGetAsync() public async Task OnGetAsync()
{ {
Book = new CreateBookViewModel(); Book = new CreateBookViewModel();
var authorLookup = await _bookAppService.GetAuthorLookupAsync(); var authorLookup = await _bookAppService.GetAuthorLookupAsync();
Authors = authorLookup.Items Authors = authorLookup.Items
.Select(x => new SelectListItem(x.Name, x.Id.ToString())) .Select(x => new SelectListItem(x.Name, x.Id.ToString()))
.ToList(); .ToList();
} }
public async Task<IActionResult> OnPostAsync() public async Task<IActionResult> OnPostAsync()
{ {
await _bookAppService.CreateAsync( await _bookAppService.CreateAsync(
ObjectMapper.Map<CreateBookViewModel, CreateUpdateBookDto>(Book) ObjectMapper.Map<CreateBookViewModel, CreateUpdateBookDto>(Book)
); );
return NoContent(); return NoContent();
} }
public class CreateBookViewModel public class CreateBookViewModel
{ {
[SelectItems(nameof(Authors))] [SelectItems(nameof(Authors))]
[DisplayName("Author")] [DisplayName("Author")]
public Guid AuthorId { get; set; } public Guid AuthorId { get; set; }
[Required] [Required]
[StringLength(128)] [StringLength(128)]
public string Name { get; set; } public string Name { get; set; }
[Required] [Required]
public BookType Type { get; set; } = BookType.Undefined; public BookType Type { get; set; } = BookType.Undefined;
[Required] [Required]
[DataType(DataType.Date)] [DataType(DataType.Date)]
public DateTime PublishDate { get; set; } = DateTime.Now; public DateTime PublishDate { get; set; } = DateTime.Now;
[Required] [Required]
public float Price { get; set; } public float Price { get; set; }
}
} }
} }
``` ```
@ -830,66 +823,65 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form;
namespace Acme.BookStore.Web.Pages.Books namespace Acme.BookStore.Web.Pages.Books;
public class EditModalModel : BookStorePageModel
{ {
public class EditModalModel : BookStorePageModel [BindProperty]
{ public EditBookViewModel Book { get; set; }
[BindProperty]
public EditBookViewModel Book { get; set; }
public List<SelectListItem> Authors { get; set; } public List<SelectListItem> Authors { get; set; }
private readonly IBookAppService _bookAppService; private readonly IBookAppService _bookAppService;
public EditModalModel(IBookAppService bookAppService) public EditModalModel(IBookAppService bookAppService)
{ {
_bookAppService = bookAppService; _bookAppService = bookAppService;
} }
public async Task OnGetAsync(Guid id) public async Task OnGetAsync(Guid id)
{ {
var bookDto = await _bookAppService.GetAsync(id); var bookDto = await _bookAppService.GetAsync(id);
Book = ObjectMapper.Map<BookDto, EditBookViewModel>(bookDto); Book = ObjectMapper.Map<BookDto, EditBookViewModel>(bookDto);
var authorLookup = await _bookAppService.GetAuthorLookupAsync(); var authorLookup = await _bookAppService.GetAuthorLookupAsync();
Authors = authorLookup.Items Authors = authorLookup.Items
.Select(x => new SelectListItem(x.Name, x.Id.ToString())) .Select(x => new SelectListItem(x.Name, x.Id.ToString()))
.ToList(); .ToList();
} }
public async Task<IActionResult> OnPostAsync() public async Task<IActionResult> OnPostAsync()
{ {
await _bookAppService.UpdateAsync( await _bookAppService.UpdateAsync(
Book.Id, Book.Id,
ObjectMapper.Map<EditBookViewModel, CreateUpdateBookDto>(Book) ObjectMapper.Map<EditBookViewModel, CreateUpdateBookDto>(Book)
); );
return NoContent(); return NoContent();
} }
public class EditBookViewModel public class EditBookViewModel
{ {
[HiddenInput] [HiddenInput]
public Guid Id { get; set; } public Guid Id { get; set; }
[SelectItems(nameof(Authors))] [SelectItems(nameof(Authors))]
[DisplayName("Author")] [DisplayName("Author")]
public Guid AuthorId { get; set; } public Guid AuthorId { get; set; }
[Required] [Required]
[StringLength(128)] [StringLength(128)]
public string Name { get; set; } public string Name { get; set; }
[Required] [Required]
public BookType Type { get; set; } = BookType.Undefined; public BookType Type { get; set; } = BookType.Undefined;
[Required] [Required]
[DataType(DataType.Date)] [DataType(DataType.Date)]
public DateTime PublishDate { get; set; } = DateTime.Now; public DateTime PublishDate { get; set; } = DateTime.Now;
[Required] [Required]
public float Price { get; set; } public float Price { get; set; }
}
} }
} }
``` ```
@ -964,13 +956,13 @@ Book list page change is trivial. Open the `/src/app/book/book.component.html` a
When you run the application, you can see the *Author* column on the table: When you run the application, you can see the *Author* column on the table:
![bookstore-books-with-authorname-angular](images/bookstore-books-with-authorname-angular.png) ![bookstore-books-with-authorname-angular](images/bookstore-books-with-authorname-angular-2.png)
### Create/Edit Forms ### Create/Edit Forms
The next step is to add an Author selection (dropdown) to the create/edit forms. The final UI will look like the one shown below: The next step is to add an Author selection (dropdown) to the create/edit forms. The final UI will look like the one shown below:
![bookstore-angular-author-selection](images/bookstore-angular-author-selection.png) ![bookstore-angular-author-selection](images/bookstore-angular-author-selection-2.png)
Added the Author dropdown as the first element in the form. Added the Author dropdown as the first element in the form.
@ -1112,7 +1104,7 @@ It is very easy to show the *Author Name* in the book list. Open the `/Pages/Boo
When you run the application, you can see the *Author* column on the table: When you run the application, you can see the *Author* column on the table:
![blazor-bookstore-book-list-with-authors](images/blazor-bookstore-book-list-with-authors.png) ![blazor-bookstore-book-list-with-authors](images/blazor-bookstore-book-list-with-authors-2.png)
### Create Book Modal ### Create Book Modal
@ -1183,7 +1175,7 @@ This requires to add a new localization key to the `en.json` file:
You can run the application to see the *Author Selection* while creating a new book: You can run the application to see the *Author Selection* while creating a new book:
![book-create-modal-with-author](images/book-create-modal-with-author.png) ![book-create-modal-with-author](images/book-create-modal-with-author-2.png)
### Edit Book Modal ### Edit Book Modal

17
docs/en/Tutorials/Part-2.md

@ -178,14 +178,13 @@ Open the `Index.cshtml` and change the whole content as shown below:
```csharp ```csharp
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Acme.BookStore.Web.Pages.Books namespace Acme.BookStore.Web.Pages.Books;
public class IndexModel : PageModel
{ {
public class IndexModel : PageModel public void OnGet()
{ {
public void OnGet()
{
}
} }
} }
``` ```
@ -537,7 +536,7 @@ Open the `/src/app/book/book.component.html` and replace the content as shown be
Now you can see the final result on your browser: Now you can see the final result on your browser:
![Book list final result](images/bookstore-book-list.png) ![Book list final result](images/bookstore-book-list-angular.png)
{{else if UI == "Blazor" || UI == "BlazorServer"}} {{else if UI == "Blazor" || UI == "BlazorServer"}}
@ -581,7 +580,7 @@ context.Menu.AddItem(
Run the project, login to the application with the username `admin` and the password `1q2w3E*` and see that the new menu item has been added to the main menu: Run the project, login to the application with the username `admin` and the password `1q2w3E*` and see that the new menu item has been added to the main menu:
![blazor-menu-bookstore](images/blazor-menu-bookstore.png) ![blazor-menu-bookstore](images/bookstore-new-menu-item-2.png)
When you click on the Books menu item under the Book Store parent, you will be redirected to the new empty Books Page. When you click on the Books menu item under the Book Store parent, you will be redirected to the new empty Books Page.
@ -621,7 +620,7 @@ Open the `Books.razor` and replace the content as the following:
Field="@nameof(BookDto.Type)" Field="@nameof(BookDto.Type)"
Caption="@L["Type"]"> Caption="@L["Type"]">
<DisplayTemplate> <DisplayTemplate>
@L[$"Enum:BookType.{Enum.GetName(context.Type)}"] @L[$"Enum:BookType.{context.Type}"]
</DisplayTemplate> </DisplayTemplate>
</DataGridColumn> </DataGridColumn>
<DataGridColumn TItem="BookDto" <DataGridColumn TItem="BookDto"
@ -664,7 +663,7 @@ We will continue benefitting from `AbpCrudPageBase` for the books page. You cou
You can run the application! The final UI of this part is shown below: You can run the application! The final UI of this part is shown below:
![blazor-bookstore-book-list](images/blazor-bookstore-book-list.png) ![blazor-bookstore-book-list](images/blazor-bookstore-book-list-2.png)
This is a fully working, server side paged, sorted and localized table of books. This is a fully working, server side paged, sorted and localized table of books.

97
docs/en/Tutorials/Part-3.md

@ -283,35 +283,34 @@ using System.Threading.Tasks;
using Acme.BookStore.Books; using Acme.BookStore.Books;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Acme.BookStore.Web.Pages.Books namespace Acme.BookStore.Web.Pages.Books;
public class EditModalModel : BookStorePageModel
{ {
public class EditModalModel : BookStorePageModel [HiddenInput]
{ [BindProperty(SupportsGet = true)]
[HiddenInput] public Guid Id { get; set; }
[BindProperty(SupportsGet = true)]
public Guid Id { get; set; }
[BindProperty] [BindProperty]
public CreateUpdateBookDto Book { get; set; } public CreateUpdateBookDto Book { get; set; }
private readonly IBookAppService _bookAppService; private readonly IBookAppService _bookAppService;
public EditModalModel(IBookAppService bookAppService) public EditModalModel(IBookAppService bookAppService)
{ {
_bookAppService = bookAppService; _bookAppService = bookAppService;
} }
public async Task OnGetAsync() public async Task OnGetAsync()
{ {
var bookDto = await _bookAppService.GetAsync(Id); var bookDto = await _bookAppService.GetAsync(Id);
Book = ObjectMapper.Map<BookDto, CreateUpdateBookDto>(bookDto); Book = ObjectMapper.Map<BookDto, CreateUpdateBookDto>(bookDto);
} }
public async Task<IActionResult> OnPostAsync() public async Task<IActionResult> OnPostAsync()
{ {
await _bookAppService.UpdateAsync(Id, Book); await _bookAppService.UpdateAsync(Id, Book);
return NoContent(); return NoContent();
}
} }
} }
```` ````
@ -327,14 +326,13 @@ To be able to map the `BookDto` to `CreateUpdateBookDto`, configure a new mappin
````csharp ````csharp
using AutoMapper; using AutoMapper;
namespace Acme.BookStore.Web namespace Acme.BookStore.Web;
public class BookStoreWebAutoMapperProfile : Profile
{ {
public class BookStoreWebAutoMapperProfile : Profile public BookStoreWebAutoMapperProfile()
{ {
public BookStoreWebAutoMapperProfile() CreateMap<BookDto, CreateUpdateBookDto>();
{
CreateMap<BookDto, CreateUpdateBookDto>();
}
} }
} }
```` ````
@ -689,7 +687,7 @@ Open `/src/app/book/book.component.html` and make the following changes:
You can open your browser and click the **New book** button to see the new modal. You can open your browser and click the **New book** button to see the new modal.
![Empty modal for new book](images/bookstore-empty-new-book-modal.png) ![Empty modal for new book](images/bookstore-empty-new-book-modal-2.png)
### Create a Reactive Form ### Create a Reactive Form
@ -926,7 +924,7 @@ export class BookComponent implements OnInit {
Now, you can open your browser to see the changes: Now, you can open your browser to see the changes:
![Save button to the modal](images/bookstore-new-book-form-v2.png) ![Save button to the modal](images/bookstore-new-book-form-v3.png)
## Updating a Book ## Updating a Book
@ -1054,7 +1052,7 @@ Open `/src/app/book/book.component.html`  and add the following `ngx-datatable-
Added an "Actions" dropdown as the first column of the table that is shown below: Added an "Actions" dropdown as the first column of the table that is shown below:
![Action buttons](images/bookstore-actions-buttons.png) ![Action buttons](images/bookstore-actions-buttons-2.png)
Also, change the `ng-template #abpHeader` section as shown below: Also, change the `ng-template #abpHeader` section as shown below:
@ -1118,11 +1116,11 @@ Open `/src/app/book/book.component.html` and modify the `ngbDropdownMenu` to add
The final actions dropdown UI looks like below: The final actions dropdown UI looks like below:
![bookstore-final-actions-dropdown](images/bookstore-final-actions-dropdown.png) ![bookstore-final-actions-dropdown](images/bookstore-final-actions-dropdown-2.png)
Clicking the "Delete" action calls the `delete` method which then shows a confirmation popup as shown below: Clicking the "Delete" action calls the `delete` method which then shows a confirmation popup as shown below:
![bookstore-confirmation-popup](images/bookstore-confirmation-popup.png) ![bookstore-confirmation-popup](images/bookstore-confirmation-popup-2.png)
{{end}} {{end}}
@ -1152,7 +1150,7 @@ Open the `Books.razor` and replace the `<CardHeader>` section with the following
This will change the card header by adding a "New book" button to the right side: This will change the card header by adding a "New book" button to the right side:
![blazor-add-book-button](images/blazor-add-book-button.png) ![blazor-add-book-button](images/blazor-add-book-button-2.png)
Now, we can add a modal that will be opened when we click the button. Now, we can add a modal that will be opened when we click the button.
@ -1187,7 +1185,7 @@ Open the `Books.razor` and add the following code to the end of the page:
@foreach (int bookTypeValue in Enum.GetValues(typeof(BookType))) @foreach (int bookTypeValue in Enum.GetValues(typeof(BookType)))
{ {
<SelectItem TValue="BookType" Value="@((BookType) bookTypeValue)"> <SelectItem TValue="BookType" Value="@((BookType) bookTypeValue)">
@L[$"Enum:BookType.{Enum.GetName((BookType)bookTypeValue)}"] @L[$"Enum:BookType.{bookTypeValue}"]
</SelectItem> </SelectItem>
} }
</Select> </Select>
@ -1226,7 +1224,7 @@ This code requires a service; Inject the `AbpBlazorMessageLocalizerHelper<T>` at
That's all. Run the application and try to add a new book: That's all. Run the application and try to add a new book:
![blazor-new-book-modal](images/blazor-new-book-modal.png) ![blazor-new-book-modal](images/blazor-new-book-modal-2.png)
## Updating a Book ## Updating a Book
@ -1252,7 +1250,7 @@ Open the `Books.razor` and add the following `DataGridEntityActionsColumn` secti
The `DataGridEntityActionsColumn` component is used to show an "Actions" dropdown for each row in the `DataGrid`. The `DataGridEntityActionsColumn` shows a **single button** instead of a dropdown if there is only one available action inside it: The `DataGridEntityActionsColumn` component is used to show an "Actions" dropdown for each row in the `DataGrid`. The `DataGridEntityActionsColumn` shows a **single button** instead of a dropdown if there is only one available action inside it:
![blazor-edit-book-action](images/blazor-edit-book-action-2.png) ![blazor-edit-book-action](images/blazor-edit-book-action-3.png)
### Edit Modal ### Edit Modal
@ -1285,7 +1283,7 @@ We can now define a modal to edit the book. Add the following code to the end of
@foreach (int bookTypeValue in Enum.GetValues(typeof(BookType))) @foreach (int bookTypeValue in Enum.GetValues(typeof(BookType)))
{ {
<SelectItem TValue="BookType" Value="@((BookType) bookTypeValue)"> <SelectItem TValue="BookType" Value="@((BookType) bookTypeValue)">
@L[$"Enum:BookType.{Enum.GetName((BookType)bookTypeValue)}"] @L[$"Enum:BookType.{bookTypeValue}"]
</SelectItem> </SelectItem>
} }
</Select> </Select>
@ -1323,14 +1321,13 @@ Open the `BookStoreBlazorAutoMapperProfile` inside the `Acme.BookStore.Blazor` p
using Acme.BookStore.Books; using Acme.BookStore.Books;
using AutoMapper; using AutoMapper;
namespace Acme.BookStore.Blazor namespace Acme.BookStore.Blazor;
public class BookStoreBlazorAutoMapperProfile : Profile
{ {
public class BookStoreBlazorAutoMapperProfile : Profile public BookStoreBlazorAutoMapperProfile()
{ {
public BookStoreBlazorAutoMapperProfile() CreateMap<BookDto, CreateUpdateBookDto>();
{
CreateMap<BookDto, CreateUpdateBookDto>();
}
} }
} }
```` ````
@ -1341,7 +1338,7 @@ namespace Acme.BookStore.Blazor
You can now run the application and try to edit a book. You can now run the application and try to edit a book.
![blazor-edit-book-modal](images/blazor-edit-book-modal.png) ![blazor-edit-book-modal](images/blazor-edit-book-modal-2.png)
> Tip: Try to leave the *Name* field empty and submit the form to show the validation error message. > Tip: Try to leave the *Name* field empty and submit the form to show the validation error message.
@ -1362,7 +1359,7 @@ Open the `Books.razor` page and add the following `EntityAction` code under the
The "Actions" button becomes a dropdown since it has two actions now: The "Actions" button becomes a dropdown since it has two actions now:
![blazor-edit-book-action](images/blazor-delete-book-action.png) ![blazor-delete-book-action](images/blazor-delete-book-action-2.png)
Run the application and try to delete a book. Run the application and try to delete a book.
@ -1422,7 +1419,7 @@ Here's the complete code to create the book management CRUD page, that has been
Field="@nameof(BookDto.Type)" Field="@nameof(BookDto.Type)"
Caption="@L["Type"]"> Caption="@L["Type"]">
<DisplayTemplate> <DisplayTemplate>
@L[$"Enum:BookType.{Enum.GetName(context.Type)}"] @L[$"Enum:BookType.{context.Type}"]
</DisplayTemplate> </DisplayTemplate>
</DataGridColumn> </DataGridColumn>
<DataGridColumn TItem="BookDto" <DataGridColumn TItem="BookDto"
@ -1474,7 +1471,7 @@ Here's the complete code to create the book management CRUD page, that has been
@foreach (int bookTypeValue in Enum.GetValues(typeof(BookType))) @foreach (int bookTypeValue in Enum.GetValues(typeof(BookType)))
{ {
<SelectItem TValue="BookType" Value="@((BookType) bookTypeValue)"> <SelectItem TValue="BookType" Value="@((BookType) bookTypeValue)">
@L[$"Enum:BookType.{Enum.GetName((BookType)bookTypeValue)}"] @L[$"Enum:BookType.{bookTypeValue}"]
</SelectItem> </SelectItem>
} }
</Select> </Select>
@ -1527,7 +1524,7 @@ Here's the complete code to create the book management CRUD page, that has been
@foreach (int bookTypeValue in Enum.GetValues(typeof(BookType))) @foreach (int bookTypeValue in Enum.GetValues(typeof(BookType)))
{ {
<SelectItem TValue="BookType" Value="@((BookType) bookTypeValue)"> <SelectItem TValue="BookType" Value="@((BookType) bookTypeValue)">
@L[$"Enum:BookType.{Enum.GetName((BookType)bookTypeValue)}"] @L[$"Enum:BookType.{bookTypeValue}"]
</SelectItem> </SelectItem>
} }
</Select> </Select>

144
docs/en/Tutorials/Part-4.md

@ -85,30 +85,31 @@ using Volo.Abp.Application.Dtos;
using Volo.Abp.Validation; using Volo.Abp.Validation;
using Xunit; using Xunit;
namespace Acme.BookStore.Books namespace Acme.BookStore.Books;
{ {{if DB=="Mongo"}}
[Collection(BookStoreTestConsts.CollectionDefinitionName)]{{end}}
public class BookAppService_Tests : BookStoreApplicationTestBase
{
private readonly IBookAppService _bookAppService;
public BookAppService_Tests() {{if DB=="Mongo"}}
{ [Collection(BookStoreTestConsts.CollectionDefinitionName)]
_bookAppService = GetRequiredService<IBookAppService>(); {{end}}
} public class BookAppService_Tests : BookStoreApplicationTestBase
{
private readonly IBookAppService _bookAppService;
[Fact] public BookAppService_Tests()
public async Task Should_Get_List_Of_Books() {
{ _bookAppService = GetRequiredService<IBookAppService>();
//Act }
var result = await _bookAppService.GetListAsync(
new PagedAndSortedResultRequestDto()
);
//Assert [Fact]
result.TotalCount.ShouldBeGreaterThan(0); public async Task Should_Get_List_Of_Books()
result.Items.ShouldContain(b => b.Name == "1984"); {
} //Act
var result = await _bookAppService.GetListAsync(
new PagedAndSortedResultRequestDto()
);
//Assert
result.TotalCount.ShouldBeGreaterThan(0);
result.Items.ShouldContain(b => b.Name == "1984");
} }
} }
```` ````
@ -176,69 +177,70 @@ using Volo.Abp.Application.Dtos;
using Volo.Abp.Validation; using Volo.Abp.Validation;
using Xunit; using Xunit;
namespace Acme.BookStore.Books namespace Acme.BookStore.Books;
{ {{if DB=="Mongo"}}
[Collection(BookStoreTestConsts.CollectionDefinitionName)]{{end}} {{if DB=="Mongo"}}
public class BookAppService_Tests : BookStoreApplicationTestBase [Collection(BookStoreTestConsts.CollectionDefinitionName)]
{{end}}
public class BookAppService_Tests : BookStoreApplicationTestBase
{
private readonly IBookAppService _bookAppService;
public BookAppService_Tests()
{ {
private readonly IBookAppService _bookAppService; _bookAppService = GetRequiredService<IBookAppService>();
}
public BookAppService_Tests() [Fact]
{ public async Task Should_Get_List_Of_Books()
_bookAppService = GetRequiredService<IBookAppService>(); {
} //Act
var result = await _bookAppService.GetListAsync(
new PagedAndSortedResultRequestDto()
);
[Fact] //Assert
public async Task Should_Get_List_Of_Books() result.TotalCount.ShouldBeGreaterThan(0);
{ result.Items.ShouldContain(b => b.Name == "1984");
//Act }
var result = await _bookAppService.GetListAsync(
new PagedAndSortedResultRequestDto() [Fact]
); public async Task Should_Create_A_Valid_Book()
{
//Act
var result = await _bookAppService.CreateAsync(
new CreateUpdateBookDto
{
Name = "New test book 42",
Price = 10,
PublishDate = DateTime.Now,
Type = BookType.ScienceFiction
}
);
//Assert //Assert
result.TotalCount.ShouldBeGreaterThan(0); result.Id.ShouldNotBe(Guid.Empty);
result.Items.ShouldContain(b => b.Name == "1984"); result.Name.ShouldBe("New test book 42");
} }
[Fact] [Fact]
public async Task Should_Create_A_Valid_Book() public async Task Should_Not_Create_A_Book_Without_Name()
{
var exception = await Assert.ThrowsAsync<AbpValidationException>(async () =>
{ {
//Act await _bookAppService.CreateAsync(
var result = await _bookAppService.CreateAsync(
new CreateUpdateBookDto new CreateUpdateBookDto
{ {
Name = "New test book 42", Name = "",
Price = 10, Price = 10,
PublishDate = DateTime.Now, PublishDate = DateTime.Now,
Type = BookType.ScienceFiction Type = BookType.ScienceFiction
} }
); );
});
//Assert exception.ValidationErrors
result.Id.ShouldNotBe(Guid.Empty); .ShouldContain(err => err.MemberNames.Any(mem => mem == "Name"));
result.Name.ShouldBe("New test book 42");
}
[Fact]
public async Task Should_Not_Create_A_Book_Without_Name()
{
var exception = await Assert.ThrowsAsync<AbpValidationException>(async () =>
{
await _bookAppService.CreateAsync(
new CreateUpdateBookDto
{
Name = "",
Price = 10,
PublishDate = DateTime.Now,
Type = BookType.ScienceFiction
}
);
});
exception.ValidationErrors
.ShouldContain(err => err.MemberNames.Any(mem => mem == "Name"));
}
} }
} }
```` ````

87
docs/en/Tutorials/Part-5.md

@ -55,19 +55,18 @@ A permission must have a unique name (a `string`). The best way is to define it
Open the `BookStorePermissions` class inside the `Acme.BookStore.Application.Contracts` project (in the `Permissions` folder) and change the content as shown below: Open the `BookStorePermissions` class inside the `Acme.BookStore.Application.Contracts` project (in the `Permissions` folder) and change the content as shown below:
````csharp ````csharp
namespace Acme.BookStore.Permissions namespace Acme.BookStore.Permissions;
public static class BookStorePermissions
{ {
public static class BookStorePermissions public const string GroupName = "BookStore";
public static class Books
{ {
public const string GroupName = "BookStore"; public const string Default = GroupName + ".Books";
public const string Create = Default + ".Create";
public static class Books public const string Edit = Default + ".Edit";
{ public const string Delete = Default + ".Delete";
public const string Default = GroupName + ".Books";
public const string Create = Default + ".Create";
public const string Edit = Default + ".Edit";
public const string Delete = Default + ".Delete";
}
} }
} }
```` ````
@ -85,24 +84,23 @@ using Acme.BookStore.Localization;
using Volo.Abp.Authorization.Permissions; using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Localization; using Volo.Abp.Localization;
namespace Acme.BookStore.Permissions namespace Acme.BookStore.Permissions;
public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvider
{ {
public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvider public override void Define(IPermissionDefinitionContext context)
{ {
public override void Define(IPermissionDefinitionContext context) var bookStoreGroup = context.AddGroup(BookStorePermissions.GroupName, L("Permission:BookStore"));
{
var bookStoreGroup = context.AddGroup(BookStorePermissions.GroupName, L("Permission:BookStore")); var booksPermission = bookStoreGroup.AddPermission(BookStorePermissions.Books.Default, L("Permission:Books"));
booksPermission.AddChild(BookStorePermissions.Books.Create, L("Permission:Books.Create"));
var booksPermission = bookStoreGroup.AddPermission(BookStorePermissions.Books.Default, L("Permission:Books")); booksPermission.AddChild(BookStorePermissions.Books.Edit, L("Permission:Books.Edit"));
booksPermission.AddChild(BookStorePermissions.Books.Create, L("Permission:Books.Create")); booksPermission.AddChild(BookStorePermissions.Books.Delete, L("Permission:Books.Delete"));
booksPermission.AddChild(BookStorePermissions.Books.Edit, L("Permission:Books.Edit")); }
booksPermission.AddChild(BookStorePermissions.Books.Delete, L("Permission:Books.Delete"));
} private static LocalizableString L(string name)
{
private static LocalizableString L(string name) return LocalizableString.Create<BookStoreResource>(name);
{
return LocalizableString.Create<BookStoreResource>(name);
}
} }
} }
```` ````
@ -148,26 +146,25 @@ using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services; using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore.Books namespace Acme.BookStore.Books;
public class BookAppService :
CrudAppService<
Book, //The Book entity
BookDto, //Used to show books
Guid, //Primary key of the book entity
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateBookDto>, //Used to create/update a book
IBookAppService //implement the IBookAppService
{ {
public class BookAppService : public BookAppService(IRepository<Book, Guid> repository)
CrudAppService< : base(repository)
Book, //The Book entity
BookDto, //Used to show books
Guid, //Primary key of the book entity
PagedAndSortedResultRequestDto, //Used for paging/sorting
CreateUpdateBookDto>, //Used to create/update a book
IBookAppService //implement the IBookAppService
{ {
public BookAppService(IRepository<Book, Guid> repository) GetPolicyName = BookStorePermissions.Books.Default;
: base(repository) GetListPolicyName = BookStorePermissions.Books.Default;
{ CreatePolicyName = BookStorePermissions.Books.Create;
GetPolicyName = BookStorePermissions.Books.Default; UpdatePolicyName = BookStorePermissions.Books.Edit;
GetListPolicyName = BookStorePermissions.Books.Default; DeletePolicyName = BookStorePermissions.Books.Delete;
CreatePolicyName = BookStorePermissions.Books.Create;
UpdatePolicyName = BookStorePermissions.Books.Edit;
DeletePolicyName = BookStorePermissions.Books.Delete;
}
} }
} }
```` ````

195
docs/en/Tutorials/Part-6.md

@ -60,45 +60,44 @@ using JetBrains.Annotations;
using Volo.Abp; using Volo.Abp;
using Volo.Abp.Domain.Entities.Auditing; using Volo.Abp.Domain.Entities.Auditing;
namespace Acme.BookStore.Authors namespace Acme.BookStore.Authors;
public class Author : FullAuditedAggregateRoot<Guid>
{ {
public class Author : FullAuditedAggregateRoot<Guid> public string Name { get; private set; }
{ public DateTime BirthDate { get; set; }
public string Name { get; private set; } public string ShortBio { get; set; }
public DateTime BirthDate { get; set; }
public string ShortBio { get; set; }
private Author() private Author()
{ {
/* This constructor is for deserialization / ORM purpose */ /* This constructor is for deserialization / ORM purpose */
} }
internal Author( internal Author(
Guid id, Guid id,
[NotNull] string name, [NotNull] string name,
DateTime birthDate, DateTime birthDate,
[CanBeNull] string shortBio = null) [CanBeNull] string shortBio = null)
: base(id) : base(id)
{ {
SetName(name); SetName(name);
BirthDate = birthDate; BirthDate = birthDate;
ShortBio = shortBio; ShortBio = shortBio;
} }
internal Author ChangeName([NotNull] string name) internal Author ChangeName([NotNull] string name)
{ {
SetName(name); SetName(name);
return this; return this;
} }
private void SetName([NotNull] string name) private void SetName([NotNull] string name)
{ {
Name = Check.NotNullOrWhiteSpace( Name = Check.NotNullOrWhiteSpace(
name, name,
nameof(name), nameof(name),
maxLength: AuthorConsts.MaxNameLength maxLength: AuthorConsts.MaxNameLength
); );
}
} }
} }
```` ````
@ -113,13 +112,13 @@ namespace Acme.BookStore.Authors
`AuthorConsts` is a simple class that is located under the `Authors` namespace (folder) of the `Acme.BookStore.Domain.Shared` project: `AuthorConsts` is a simple class that is located under the `Authors` namespace (folder) of the `Acme.BookStore.Domain.Shared` project:
````csharp ````csharp
namespace Acme.BookStore.Authors namespace Acme.BookStore.Authors;
public static class AuthorConsts
{ {
public static class AuthorConsts public const int MaxNameLength = 64;
{
public const int MaxNameLength = 64;
}
} }
```` ````
Created this class inside the `Acme.BookStore.Domain.Shared` project since we will re-use it on the [Data Transfer Objects](../Data-Transfer-Objects.md) (DTOs) later. Created this class inside the `Acme.BookStore.Domain.Shared` project since we will re-use it on the [Data Transfer Objects](../Data-Transfer-Objects.md) (DTOs) later.
@ -135,53 +134,52 @@ using JetBrains.Annotations;
using Volo.Abp; using Volo.Abp;
using Volo.Abp.Domain.Services; using Volo.Abp.Domain.Services;
namespace Acme.BookStore.Authors namespace Acme.BookStore.Authors;
public class AuthorManager : DomainService
{ {
public class AuthorManager : DomainService private readonly IAuthorRepository _authorRepository;
public AuthorManager(IAuthorRepository authorRepository)
{ {
private readonly IAuthorRepository _authorRepository; _authorRepository = authorRepository;
}
public AuthorManager(IAuthorRepository authorRepository) public async Task<Author> CreateAsync(
{ [NotNull] string name,
_authorRepository = authorRepository; DateTime birthDate,
} [CanBeNull] string shortBio = null)
{
Check.NotNullOrWhiteSpace(name, nameof(name));
public async Task<Author> CreateAsync( var existingAuthor = await _authorRepository.FindByNameAsync(name);
[NotNull] string name, if (existingAuthor != null)
DateTime birthDate,
[CanBeNull] string shortBio = null)
{ {
Check.NotNullOrWhiteSpace(name, nameof(name)); throw new AuthorAlreadyExistsException(name);
var existingAuthor = await _authorRepository.FindByNameAsync(name);
if (existingAuthor != null)
{
throw new AuthorAlreadyExistsException(name);
}
return new Author(
GuidGenerator.Create(),
name,
birthDate,
shortBio
);
} }
public async Task ChangeNameAsync( return new Author(
[NotNull] Author author, GuidGenerator.Create(),
[NotNull] string newName) name,
{ birthDate,
Check.NotNull(author, nameof(author)); shortBio
Check.NotNullOrWhiteSpace(newName, nameof(newName)); );
}
var existingAuthor = await _authorRepository.FindByNameAsync(newName); public async Task ChangeNameAsync(
if (existingAuthor != null && existingAuthor.Id != author.Id) [NotNull] Author author,
{ [NotNull] string newName)
throw new AuthorAlreadyExistsException(newName); {
} Check.NotNull(author, nameof(author));
Check.NotNullOrWhiteSpace(newName, nameof(newName));
author.ChangeName(newName); var existingAuthor = await _authorRepository.FindByNameAsync(newName);
if (existingAuthor != null && existingAuthor.Id != author.Id)
{
throw new AuthorAlreadyExistsException(newName);
} }
author.ChangeName(newName);
} }
} }
```` ````
@ -195,15 +193,14 @@ Both methods checks if there is already an author with the given name and throws
````csharp ````csharp
using Volo.Abp; using Volo.Abp;
namespace Acme.BookStore.Authors namespace Acme.BookStore.Authors;
public class AuthorAlreadyExistsException : BusinessException
{ {
public class AuthorAlreadyExistsException : BusinessException public AuthorAlreadyExistsException(string name)
: base(BookStoreDomainErrorCodes.AuthorAlreadyExists)
{ {
public AuthorAlreadyExistsException(string name) WithData("name", name);
: base(BookStoreDomainErrorCodes.AuthorAlreadyExists)
{
WithData("name", name);
}
} }
} }
```` ````
@ -213,12 +210,11 @@ namespace Acme.BookStore.Authors
Open the `BookStoreDomainErrorCodes` in the `Acme.BookStore.Domain.Shared` project and change as shown below: Open the `BookStoreDomainErrorCodes` in the `Acme.BookStore.Domain.Shared` project and change as shown below:
````csharp ````csharp
namespace Acme.BookStore namespace Acme.BookStore;
public static class BookStoreDomainErrorCodes
{ {
public static class BookStoreDomainErrorCodes public const string AuthorAlreadyExists = "BookStore:00001";
{
public const string AuthorAlreadyExists = "BookStore:00001";
}
} }
```` ````
@ -240,19 +236,18 @@ using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore.Authors namespace Acme.BookStore.Authors;
{
public interface IAuthorRepository : IRepository<Author, Guid>
{
Task<Author> FindByNameAsync(string name);
Task<List<Author>> GetListAsync( public interface IAuthorRepository : IRepository<Author, Guid>
int skipCount, {
int maxResultCount, Task<Author> FindByNameAsync(string name);
string sorting,
string filter = null Task<List<Author>> GetListAsync(
); int skipCount,
} int maxResultCount,
string sorting,
string filter = null
);
} }
```` ````

132
docs/en/Tutorials/Part-7.md

@ -121,41 +121,40 @@ using Microsoft.EntityFrameworkCore;
using Volo.Abp.Domain.Repositories.EntityFrameworkCore; using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore;
namespace Acme.BookStore.Authors namespace Acme.BookStore.Authors;
public class EfCoreAuthorRepository
: EfCoreRepository<BookStoreDbContext, Author, Guid>,
IAuthorRepository
{ {
public class EfCoreAuthorRepository public EfCoreAuthorRepository(
: EfCoreRepository<BookStoreDbContext, Author, Guid>, IDbContextProvider<BookStoreDbContext> dbContextProvider)
IAuthorRepository : base(dbContextProvider)
{
}
public async Task<Author> FindByNameAsync(string name)
{ {
public EfCoreAuthorRepository( var dbSet = await GetDbSetAsync();
IDbContextProvider<BookStoreDbContext> dbContextProvider) return await dbSet.FirstOrDefaultAsync(author => author.Name == name);
: base(dbContextProvider) }
{
} public async Task<List<Author>> GetListAsync(
int skipCount,
public async Task<Author> FindByNameAsync(string name) int maxResultCount,
{ string sorting,
var dbSet = await GetDbSetAsync(); string filter = null)
return await dbSet.FirstOrDefaultAsync(author => author.Name == name); {
} var dbSet = await GetDbSetAsync();
return await dbSet
public async Task<List<Author>> GetListAsync( .WhereIf(
int skipCount, !filter.IsNullOrWhiteSpace(),
int maxResultCount, author => author.Name.Contains(filter)
string sorting, )
string filter = null) .OrderBy(sorting)
{ .Skip(skipCount)
var dbSet = await GetDbSetAsync(); .Take(maxResultCount)
return await dbSet .ToListAsync();
.WhereIf(
!filter.IsNullOrWhiteSpace(),
author => author.Name.Contains(filter)
)
.OrderBy(sorting)
.Skip(skipCount)
.Take(maxResultCount)
.ToListAsync();
}
} }
} }
```` ````
@ -182,42 +181,41 @@ using MongoDB.Driver.Linq;
using Volo.Abp.Domain.Repositories.MongoDB; using Volo.Abp.Domain.Repositories.MongoDB;
using Volo.Abp.MongoDB; using Volo.Abp.MongoDB;
namespace Acme.BookStore.Authors namespace Acme.BookStore.Authors;
public class MongoDbAuthorRepository
: MongoDbRepository<BookStoreMongoDbContext, Author, Guid>,
IAuthorRepository
{ {
public class MongoDbAuthorRepository public MongoDbAuthorRepository(
: MongoDbRepository<BookStoreMongoDbContext, Author, Guid>, IMongoDbContextProvider<BookStoreMongoDbContext> dbContextProvider
IAuthorRepository ) : base(dbContextProvider)
{ {
public MongoDbAuthorRepository( }
IMongoDbContextProvider<BookStoreMongoDbContext> dbContextProvider
) : base(dbContextProvider) public async Task<Author> FindByNameAsync(string name)
{ {
} var queryable = await GetMongoQueryableAsync();
return await queryable.FirstOrDefaultAsync(author => author.Name == name);
public async Task<Author> FindByNameAsync(string name) }
{
var queryable = await GetMongoQueryableAsync(); public async Task<List<Author>> GetListAsync(
return await queryable.FirstOrDefaultAsync(author => author.Name == name); int skipCount,
} int maxResultCount,
string sorting,
public async Task<List<Author>> GetListAsync( string filter = null)
int skipCount, {
int maxResultCount, var queryable = await GetMongoQueryableAsync();
string sorting, return await queryable
string filter = null) .WhereIf<Author, IMongoQueryable<Author>>(
{ !filter.IsNullOrWhiteSpace(),
var queryable = await GetMongoQueryableAsync(); author => author.Name.Contains(filter)
return await queryable )
.WhereIf<Author, IMongoQueryable<Author>>( .OrderBy(sorting)
!filter.IsNullOrWhiteSpace(), .As<IMongoQueryable<Author>>()
author => author.Name.Contains(filter) .Skip(skipCount)
) .Take(maxResultCount)
.OrderBy(sorting) .ToListAsync();
.As<IMongoQueryable<Author>>()
.Skip(skipCount)
.Take(maxResultCount)
.ToListAsync();
}
} }
} }
``` ```

379
docs/en/Tutorials/Part-8.md

@ -50,20 +50,19 @@ using System.Threading.Tasks;
using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services; using Volo.Abp.Application.Services;
namespace Acme.BookStore.Authors namespace Acme.BookStore.Authors;
public interface IAuthorAppService : IApplicationService
{ {
public interface IAuthorAppService : IApplicationService Task<AuthorDto> GetAsync(Guid id);
{
Task<AuthorDto> GetAsync(Guid id);
Task<PagedResultDto<AuthorDto>> GetListAsync(GetAuthorListDto input); Task<PagedResultDto<AuthorDto>> GetListAsync(GetAuthorListDto input);
Task<AuthorDto> CreateAsync(CreateAuthorDto input); Task<AuthorDto> CreateAsync(CreateAuthorDto input);
Task UpdateAsync(Guid id, UpdateAuthorDto input); Task UpdateAsync(Guid id, UpdateAuthorDto input);
Task DeleteAsync(Guid id); Task DeleteAsync(Guid id);
}
} }
```` ````
@ -80,16 +79,15 @@ This interface is using the DTOs defined below (create them for your project).
using System; using System;
using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Dtos;
namespace Acme.BookStore.Authors namespace Acme.BookStore.Authors;
public class AuthorDto : EntityDto<Guid>
{ {
public class AuthorDto : EntityDto<Guid> public string Name { get; set; }
{
public string Name { get; set; }
public DateTime BirthDate { get; set; } public DateTime BirthDate { get; set; }
public string ShortBio { get; set; } public string ShortBio { get; set; }
}
} }
```` ````
@ -100,12 +98,11 @@ namespace Acme.BookStore.Authors
````csharp ````csharp
using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Dtos;
namespace Acme.BookStore.Authors namespace Acme.BookStore.Authors;
public class GetAuthorListDto : PagedAndSortedResultRequestDto
{ {
public class GetAuthorListDto : PagedAndSortedResultRequestDto public string Filter { get; set; }
{
public string Filter { get; set; }
}
} }
```` ````
@ -120,19 +117,18 @@ namespace Acme.BookStore.Authors
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace Acme.BookStore.Authors namespace Acme.BookStore.Authors;
{
public class CreateAuthorDto
{
[Required]
[StringLength(AuthorConsts.MaxNameLength)]
public string Name { get; set; }
[Required] public class CreateAuthorDto
public DateTime BirthDate { get; set; } {
[Required]
public string ShortBio { get; set; } [StringLength(AuthorConsts.MaxNameLength)]
} public string Name { get; set; }
[Required]
public DateTime BirthDate { get; set; }
public string ShortBio { get; set; }
} }
```` ````
@ -144,19 +140,18 @@ Data annotation attributes can be used to validate the DTO. See the [validation
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace Acme.BookStore.Authors namespace Acme.BookStore.Authors;
{
public class UpdateAuthorDto
{
[Required]
[StringLength(AuthorConsts.MaxNameLength)]
public string Name { get; set; }
[Required] public class UpdateAuthorDto
public DateTime BirthDate { get; set; } {
[Required]
public string ShortBio { get; set; } [StringLength(AuthorConsts.MaxNameLength)]
} public string Name { get; set; }
[Required]
public DateTime BirthDate { get; set; }
public string ShortBio { get; set; }
} }
```` ````
@ -176,24 +171,23 @@ using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Dtos;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore.Authors namespace Acme.BookStore.Authors;
{
[Authorize(BookStorePermissions.Authors.Default)]
public class AuthorAppService : BookStoreAppService, IAuthorAppService
{
private readonly IAuthorRepository _authorRepository;
private readonly AuthorManager _authorManager;
public AuthorAppService( [Authorize(BookStorePermissions.Authors.Default)]
IAuthorRepository authorRepository, public class AuthorAppService : BookStoreAppService, IAuthorAppService
AuthorManager authorManager) {
{ private readonly IAuthorRepository _authorRepository;
_authorRepository = authorRepository; private readonly AuthorManager _authorManager;
_authorManager = authorManager;
}
//...SERVICE METHODS WILL COME HERE... public AuthorAppService(
IAuthorRepository authorRepository,
AuthorManager authorManager)
{
_authorRepository = authorRepository;
_authorManager = authorManager;
} }
//...SERVICE METHODS WILL COME HERE...
} }
```` ````
@ -327,28 +321,27 @@ You can't compile the code since it is expecting some constants declared in the
Open the `BookStorePermissions` class inside the `Acme.BookStore.Application.Contracts` project (in the `Permissions` folder) and change the content as shown below: Open the `BookStorePermissions` class inside the `Acme.BookStore.Application.Contracts` project (in the `Permissions` folder) and change the content as shown below:
````csharp ````csharp
namespace Acme.BookStore.Permissions namespace Acme.BookStore.Permissions;
public static class BookStorePermissions
{ {
public static class BookStorePermissions public const string GroupName = "BookStore";
{
public const string GroupName = "BookStore";
public static class Books public static class Books
{ {
public const string Default = GroupName + ".Books"; public const string Default = GroupName + ".Books";
public const string Create = Default + ".Create"; public const string Create = Default + ".Create";
public const string Edit = Default + ".Edit"; public const string Edit = Default + ".Edit";
public const string Delete = Default + ".Delete"; public const string Delete = Default + ".Delete";
} }
// *** ADDED a NEW NESTED CLASS *** // *** ADDED a NEW NESTED CLASS ***
public static class Authors public static class Authors
{ {
public const string Default = GroupName + ".Authors"; public const string Default = GroupName + ".Authors";
public const string Create = Default + ".Create"; public const string Create = Default + ".Create";
public const string Edit = Default + ".Edit"; public const string Edit = Default + ".Edit";
public const string Delete = Default + ".Delete"; public const string Delete = Default + ".Delete";
}
} }
} }
```` ````
@ -400,72 +393,71 @@ using Volo.Abp.Data;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore namespace Acme.BookStore;
public class BookStoreDataSeederContributor
: IDataSeedContributor, ITransientDependency
{ {
public class BookStoreDataSeederContributor private readonly IRepository<Book, Guid> _bookRepository;
: IDataSeedContributor, ITransientDependency private readonly IAuthorRepository _authorRepository;
private readonly AuthorManager _authorManager;
public BookStoreDataSeederContributor(
IRepository<Book, Guid> bookRepository,
IAuthorRepository authorRepository,
AuthorManager authorManager)
{ {
private readonly IRepository<Book, Guid> _bookRepository; _bookRepository = bookRepository;
private readonly IAuthorRepository _authorRepository; _authorRepository = authorRepository;
private readonly AuthorManager _authorManager; _authorManager = authorManager;
}
public BookStoreDataSeederContributor(
IRepository<Book, Guid> bookRepository, public async Task SeedAsync(DataSeedContext context)
IAuthorRepository authorRepository, {
AuthorManager authorManager) if (await _bookRepository.GetCountAsync() <= 0)
{ {
_bookRepository = bookRepository; await _bookRepository.InsertAsync(
_authorRepository = authorRepository; new Book
_authorManager = authorManager; {
Name = "1984",
Type = BookType.Dystopia,
PublishDate = new DateTime(1949, 6, 8),
Price = 19.84f
},
autoSave: true
);
await _bookRepository.InsertAsync(
new Book
{
Name = "The Hitchhiker's Guide to the Galaxy",
Type = BookType.ScienceFiction,
PublishDate = new DateTime(1995, 9, 27),
Price = 42.0f
},
autoSave: true
);
} }
public async Task SeedAsync(DataSeedContext context) // ADDED SEED DATA FOR AUTHORS
{
if (await _bookRepository.GetCountAsync() <= 0)
{
await _bookRepository.InsertAsync(
new Book
{
Name = "1984",
Type = BookType.Dystopia,
PublishDate = new DateTime(1949, 6, 8),
Price = 19.84f
},
autoSave: true
);
await _bookRepository.InsertAsync(
new Book
{
Name = "The Hitchhiker's Guide to the Galaxy",
Type = BookType.ScienceFiction,
PublishDate = new DateTime(1995, 9, 27),
Price = 42.0f
},
autoSave: true
);
}
// ADDED SEED DATA FOR AUTHORS if (await _authorRepository.GetCountAsync() <= 0)
{
await _authorRepository.InsertAsync(
await _authorManager.CreateAsync(
"George Orwell",
new DateTime(1903, 06, 25),
"Orwell produced literary criticism and poetry, fiction and polemical journalism; and is best known for the allegorical novella Animal Farm (1945) and the dystopian novel Nineteen Eighty-Four (1949)."
)
);
if (await _authorRepository.GetCountAsync() <= 0) await _authorRepository.InsertAsync(
{ await _authorManager.CreateAsync(
await _authorRepository.InsertAsync( "Douglas Adams",
await _authorManager.CreateAsync( new DateTime(1952, 03, 11),
"George Orwell", "Douglas Adams was an English author, screenwriter, essayist, humorist, satirist and dramatist. Adams was an advocate for environmentalism and conservation, a lover of fast cars, technological innovation and the Apple Macintosh, and a self-proclaimed 'radical atheist'."
new DateTime(1903, 06, 25), )
"Orwell produced literary criticism and poetry, fiction and polemical journalism; and is best known for the allegorical novella Animal Farm (1945) and the dystopian novel Nineteen Eighty-Four (1949)." );
)
);
await _authorRepository.InsertAsync(
await _authorManager.CreateAsync(
"Douglas Adams",
new DateTime(1952, 03, 11),
"Douglas Adams was an English author, screenwriter, essayist, humorist, satirist and dramatist. Adams was an advocate for environmentalism and conservation, a lover of fast cars, technological innovation and the Apple Macintosh, and a self-proclaimed 'radical atheist'."
)
);
}
} }
} }
} }
@ -491,73 +483,74 @@ using System.Threading.Tasks;
using Shouldly; using Shouldly;
using Xunit; using Xunit;
namespace Acme.BookStore.Authors namespace Acme.BookStore.Authors;
{ {{if DB=="Mongo"}}
[Collection(BookStoreTestConsts.CollectionDefinitionName)]{{end}} {{if DB=="Mongo"}}
public class AuthorAppService_Tests : BookStoreApplicationTestBase [Collection(BookStoreTestConsts.CollectionDefinitionName)]
{{end}}
public class AuthorAppService_Tests : BookStoreApplicationTestBase
{
private readonly IAuthorAppService _authorAppService;
public AuthorAppService_Tests()
{ {
private readonly IAuthorAppService _authorAppService; _authorAppService = GetRequiredService<IAuthorAppService>();
}
public AuthorAppService_Tests() [Fact]
{ public async Task Should_Get_All_Authors_Without_Any_Filter()
_authorAppService = GetRequiredService<IAuthorAppService>(); {
} var result = await _authorAppService.GetListAsync(new GetAuthorListDto());
[Fact] result.TotalCount.ShouldBeGreaterThanOrEqualTo(2);
public async Task Should_Get_All_Authors_Without_Any_Filter() result.Items.ShouldContain(author => author.Name == "George Orwell");
{ result.Items.ShouldContain(author => author.Name == "Douglas Adams");
var result = await _authorAppService.GetListAsync(new GetAuthorListDto()); }
result.TotalCount.ShouldBeGreaterThanOrEqualTo(2); [Fact]
result.Items.ShouldContain(author => author.Name == "George Orwell"); public async Task Should_Get_Filtered_Authors()
result.Items.ShouldContain(author => author.Name == "Douglas Adams"); {
} var result = await _authorAppService.GetListAsync(
new GetAuthorListDto {Filter = "George"});
[Fact] result.TotalCount.ShouldBeGreaterThanOrEqualTo(1);
public async Task Should_Get_Filtered_Authors() result.Items.ShouldContain(author => author.Name == "George Orwell");
{ result.Items.ShouldNotContain(author => author.Name == "Douglas Adams");
var result = await _authorAppService.GetListAsync( }
new GetAuthorListDto {Filter = "George"});
result.TotalCount.ShouldBeGreaterThanOrEqualTo(1); [Fact]
result.Items.ShouldContain(author => author.Name == "George Orwell"); public async Task Should_Create_A_New_Author()
result.Items.ShouldNotContain(author => author.Name == "Douglas Adams"); {
} var authorDto = await _authorAppService.CreateAsync(
new CreateAuthorDto
{
Name = "Edward Bellamy",
BirthDate = new DateTime(1850, 05, 22),
ShortBio = "Edward Bellamy was an American author..."
}
);
authorDto.Id.ShouldNotBe(Guid.Empty);
authorDto.Name.ShouldBe("Edward Bellamy");
}
[Fact] [Fact]
public async Task Should_Create_A_New_Author() public async Task Should_Not_Allow_To_Create_Duplicate_Author()
{
await Assert.ThrowsAsync<AuthorAlreadyExistsException>(async () =>
{ {
var authorDto = await _authorAppService.CreateAsync( await _authorAppService.CreateAsync(
new CreateAuthorDto new CreateAuthorDto
{ {
Name = "Edward Bellamy", Name = "Douglas Adams",
BirthDate = new DateTime(1850, 05, 22), BirthDate = DateTime.Now,
ShortBio = "Edward Bellamy was an American author..." ShortBio = "..."
} }
); );
});
authorDto.Id.ShouldNotBe(Guid.Empty);
authorDto.Name.ShouldBe("Edward Bellamy");
}
[Fact]
public async Task Should_Not_Allow_To_Create_Duplicate_Author()
{
await Assert.ThrowsAsync<AuthorAlreadyExistsException>(async () =>
{
await _authorAppService.CreateAsync(
new CreateAuthorDto
{
Name = "Douglas Adams",
BirthDate = DateTime.Now,
ShortBio = "..."
}
);
});
}
//TODO: Test other methods...
} }
//TODO: Test other methods...
} }
```` ````

416
docs/en/Tutorials/Part-9.md

@ -95,14 +95,13 @@ This is a simple page similar to the Books page we had created before. It import
````csharp ````csharp
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Acme.BookStore.Web.Pages.Authors namespace Acme.BookStore.Web.Pages.Authors;
public class IndexModel : PageModel
{ {
public class IndexModel : PageModel public void OnGet()
{ {
public void OnGet()
{
}
} }
} }
```` ````
@ -248,11 +247,11 @@ context.Menu.AddItem(
Run and login to the application. **You can not see the menu item since you don't have permission yet.** Go to the `Identity/Roles` page, click to the *Actions* button and select the *Permissions* action for the **admin role**: Run and login to the application. **You can not see the menu item since you don't have permission yet.** Go to the `Identity/Roles` page, click to the *Actions* button and select the *Permissions* action for the **admin role**:
![bookstore-author-permissions](images/bookstore-author-permissions-2.png) ![bookstore-author-permissions](images/bookstore-author-permissions-3.png)
As you see, the admin role has no *Author Management* permissions yet. Click to the checkboxes and save the modal to grant the necessary permissions. You will see the *Authors* menu item under the *Book Store* in the main menu, after **refreshing the page**: As you see, the admin role has no *Author Management* permissions yet. Click to the checkboxes and save the modal to grant the necessary permissions. You will see the *Authors* menu item under the *Book Store* in the main menu, after **refreshing the page**:
![bookstore-authors-page](images/bookstore-authors-page-2.png) ![bookstore-authors-page](images/bookstore-authors-page-3.png)
The page is fully working except *New author* and *Actions/Edit* since we haven't implemented them yet. The page is fully working except *New author* and *Actions/Edit* since we haven't implemented them yet.
@ -302,45 +301,44 @@ using Acme.BookStore.Authors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form;
namespace Acme.BookStore.Web.Pages.Authors namespace Acme.BookStore.Web.Pages.Authors;
public class CreateModalModel : BookStorePageModel
{ {
public class CreateModalModel : BookStorePageModel [BindProperty]
{ public CreateAuthorViewModel Author { get; set; }
[BindProperty]
public CreateAuthorViewModel Author { get; set; }
private readonly IAuthorAppService _authorAppService; private readonly IAuthorAppService _authorAppService;
public CreateModalModel(IAuthorAppService authorAppService) public CreateModalModel(IAuthorAppService authorAppService)
{ {
_authorAppService = authorAppService; _authorAppService = authorAppService;
} }
public void OnGet() public void OnGet()
{ {
Author = new CreateAuthorViewModel(); Author = new CreateAuthorViewModel();
} }
public async Task<IActionResult> OnPostAsync() public async Task<IActionResult> OnPostAsync()
{ {
var dto = ObjectMapper.Map<CreateAuthorViewModel, CreateAuthorDto>(Author); var dto = ObjectMapper.Map<CreateAuthorViewModel, CreateAuthorDto>(Author);
await _authorAppService.CreateAsync(dto); await _authorAppService.CreateAsync(dto);
return NoContent(); return NoContent();
} }
public class CreateAuthorViewModel public class CreateAuthorViewModel
{ {
[Required] [Required]
[StringLength(AuthorConsts.MaxNameLength)] [StringLength(AuthorConsts.MaxNameLength)]
public string Name { get; set; } public string Name { get; set; }
[Required] [Required]
[DataType(DataType.Date)] [DataType(DataType.Date)]
public DateTime BirthDate { get; set; } public DateTime BirthDate { get; set; }
[TextArea] [TextArea]
public string ShortBio { get; set; } public string ShortBio { get; set; }
}
} }
} }
``` ```
@ -359,18 +357,17 @@ using Acme.BookStore.Authors; // ADDED NAMESPACE IMPORT
using Acme.BookStore.Books; using Acme.BookStore.Books;
using AutoMapper; using AutoMapper;
namespace Acme.BookStore.Web namespace Acme.BookStore.Web;
public class BookStoreWebAutoMapperProfile : Profile
{ {
public class BookStoreWebAutoMapperProfile : Profile public BookStoreWebAutoMapperProfile()
{ {
public BookStoreWebAutoMapperProfile() CreateMap<BookDto, CreateUpdateBookDto>();
{
CreateMap<BookDto, CreateUpdateBookDto>();
// ADD a NEW MAPPING // ADD a NEW MAPPING
CreateMap<Pages.Authors.CreateModalModel.CreateAuthorViewModel, CreateMap<Pages.Authors.CreateModalModel.CreateAuthorViewModel,
CreateAuthorDto>(); CreateAuthorDto>();
}
} }
} }
```` ````
@ -420,52 +417,51 @@ using Acme.BookStore.Authors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form;
namespace Acme.BookStore.Web.Pages.Authors namespace Acme.BookStore.Web.Pages.Authors;
public class EditModalModel : BookStorePageModel
{ {
public class EditModalModel : BookStorePageModel [BindProperty]
{ public EditAuthorViewModel Author { get; set; }
[BindProperty]
public EditAuthorViewModel Author { get; set; }
private readonly IAuthorAppService _authorAppService; private readonly IAuthorAppService _authorAppService;
public EditModalModel(IAuthorAppService authorAppService) public EditModalModel(IAuthorAppService authorAppService)
{ {
_authorAppService = authorAppService; _authorAppService = authorAppService;
} }
public async Task OnGetAsync(Guid id) public async Task OnGetAsync(Guid id)
{ {
var authorDto = await _authorAppService.GetAsync(id); var authorDto = await _authorAppService.GetAsync(id);
Author = ObjectMapper.Map<AuthorDto, EditAuthorViewModel>(authorDto); Author = ObjectMapper.Map<AuthorDto, EditAuthorViewModel>(authorDto);
} }
public async Task<IActionResult> OnPostAsync() public async Task<IActionResult> OnPostAsync()
{ {
await _authorAppService.UpdateAsync( await _authorAppService.UpdateAsync(
Author.Id, Author.Id,
ObjectMapper.Map<EditAuthorViewModel, UpdateAuthorDto>(Author) ObjectMapper.Map<EditAuthorViewModel, UpdateAuthorDto>(Author)
); );
return NoContent(); return NoContent();
} }
public class EditAuthorViewModel public class EditAuthorViewModel
{ {
[HiddenInput] [HiddenInput]
public Guid Id { get; set; } public Guid Id { get; set; }
[Required] [Required]
[StringLength(AuthorConsts.MaxNameLength)] [StringLength(AuthorConsts.MaxNameLength)]
public string Name { get; set; } public string Name { get; set; }
[Required] [Required]
[DataType(DataType.Date)] [DataType(DataType.Date)]
public DateTime BirthDate { get; set; } public DateTime BirthDate { get; set; }
[TextArea] [TextArea]
public string ShortBio { get; set; } public string ShortBio { get; set; }
}
} }
} }
``` ```
@ -482,22 +478,21 @@ using Acme.BookStore.Authors;
using Acme.BookStore.Books; using Acme.BookStore.Books;
using AutoMapper; using AutoMapper;
namespace Acme.BookStore.Web namespace Acme.BookStore.Web;
public class BookStoreWebAutoMapperProfile : Profile
{ {
public class BookStoreWebAutoMapperProfile : Profile public BookStoreWebAutoMapperProfile()
{ {
public BookStoreWebAutoMapperProfile() CreateMap<BookDto, CreateUpdateBookDto>();
{
CreateMap<BookDto, CreateUpdateBookDto>();
CreateMap<Pages.Authors.CreateModalModel.CreateAuthorViewModel, CreateMap<Pages.Authors.CreateModalModel.CreateAuthorViewModel,
CreateAuthorDto>(); CreateAuthorDto>();
// ADD THESE NEW MAPPINGS // ADD THESE NEW MAPPINGS
CreateMap<AuthorDto, Pages.Authors.EditModalModel.EditAuthorViewModel>(); CreateMap<AuthorDto, Pages.Authors.EditModalModel.EditAuthorViewModel>();
CreateMap<Pages.Authors.EditModalModel.EditAuthorViewModel, CreateMap<Pages.Authors.EditModalModel.EditAuthorViewModel,
UpdateAuthorDto>(); UpdateAuthorDto>();
}
} }
} }
``` ```
@ -833,11 +828,11 @@ This page uses some localization keys we need to declare. Open the `en.json` fil
Run and login to the application. **You can not see the menu item since you don't have permission yet.** Go to the `identity/roles` page, click to the *Actions* button and select the *Permissions* action for the **admin role**: Run and login to the application. **You can not see the menu item since you don't have permission yet.** Go to the `identity/roles` page, click to the *Actions* button and select the *Permissions* action for the **admin role**:
![bookstore-author-permissions](images/bookstore-author-permissions.png) ![bookstore-author-permissions](images/bookstore-author-permissions-2.png)
As you see, the admin role has no *Author Management* permissions yet. Click to the checkboxes and save the modal to grant the necessary permissions. You will see the *Authors* menu item under the *Book Store* in the main menu, after **refreshing the page**: As you see, the admin role has no *Author Management* permissions yet. Click to the checkboxes and save the modal to grant the necessary permissions. You will see the *Authors* menu item under the *Book Store* in the main menu, after **refreshing the page**:
![bookstore-authors-page](images/bookstore-angular-authors-page.png) ![bookstore-authors-page](images/bookstore-angular-authors-page-2.png)
That's all! This is a fully working CRUD page, you can create, edit and delete authors. That's all! This is a fully working CRUD page, you can create, edit and delete authors.
@ -863,12 +858,11 @@ Create a new Razor Component Page, `/Pages/Authors.razor`, in the `Acme.BookStor
@inject AbpBlazorMessageLocalizerHelper<BookStoreResource> LH @inject AbpBlazorMessageLocalizerHelper<BookStoreResource> LH
<Card> <Card>
<CardHeader> <CardHeader>
<Row> <Row Class="justify-content-between">
<Column ColumnSize="ColumnSize.Is6"> <Column ColumnSize="ColumnSize.IsAuto">
<h2>@L["Authors"]</h2> <h2>@L["Authors"]</h2>
</Column> </Column>
<Column ColumnSize="ColumnSize.Is6"> <Column ColumnSize="ColumnSize.IsAuto">
<Paragraph Alignment="TextAlignment.Right">
@if (CanCreateAuthor) @if (CanCreateAuthor)
{ {
<Button Color="Color.Primary" <Button Color="Color.Primary"
@ -876,7 +870,6 @@ Create a new Razor Component Page, `/Pages/Authors.razor`, in the `Acme.BookStor
@L["NewAuthor"] @L["NewAuthor"]
</Button> </Button>
} }
</Paragraph>
</Column> </Column>
</Row> </Row>
</CardHeader> </CardHeader>
@ -1054,142 +1047,141 @@ using Blazorise.DataGrid;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Dtos;
namespace Acme.BookStore.Blazor.Pages namespace Acme.BookStore.Blazor.Pages;
public partial class Authors
{ {
public partial class Authors private IReadOnlyList<AuthorDto> AuthorList { get; set; }
{
private IReadOnlyList<AuthorDto> AuthorList { get; set; }
private int PageSize { get; } = LimitedResultRequestDto.DefaultMaxResultCount; private int PageSize { get; } = LimitedResultRequestDto.DefaultMaxResultCount;
private int CurrentPage { get; set; } private int CurrentPage { get; set; }
private string CurrentSorting { get; set; } private string CurrentSorting { get; set; }
private int TotalCount { get; set; } private int TotalCount { get; set; }
private bool CanCreateAuthor { get; set; } private bool CanCreateAuthor { get; set; }
private bool CanEditAuthor { get; set; } private bool CanEditAuthor { get; set; }
private bool CanDeleteAuthor { get; set; } private bool CanDeleteAuthor { get; set; }
private CreateAuthorDto NewAuthor { get; set; } private CreateAuthorDto NewAuthor { get; set; }
private Guid EditingAuthorId { get; set; } private Guid EditingAuthorId { get; set; }
private UpdateAuthorDto EditingAuthor { get; set; } private UpdateAuthorDto EditingAuthor { get; set; }
private Modal CreateAuthorModal { get; set; } private Modal CreateAuthorModal { get; set; }
private Modal EditAuthorModal { get; set; } private Modal EditAuthorModal { get; set; }
private Validations CreateValidationsRef; private Validations CreateValidationsRef;
private Validations EditValidationsRef; private Validations EditValidationsRef;
public Authors() public Authors()
{ {
NewAuthor = new CreateAuthorDto(); NewAuthor = new CreateAuthorDto();
EditingAuthor = new UpdateAuthorDto(); EditingAuthor = new UpdateAuthorDto();
} }
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
await SetPermissionsAsync(); await SetPermissionsAsync();
await GetAuthorsAsync(); await GetAuthorsAsync();
} }
private async Task SetPermissionsAsync() private async Task SetPermissionsAsync()
{ {
CanCreateAuthor = await AuthorizationService CanCreateAuthor = await AuthorizationService
.IsGrantedAsync(BookStorePermissions.Authors.Create); .IsGrantedAsync(BookStorePermissions.Authors.Create);
CanEditAuthor = await AuthorizationService CanEditAuthor = await AuthorizationService
.IsGrantedAsync(BookStorePermissions.Authors.Edit); .IsGrantedAsync(BookStorePermissions.Authors.Edit);
CanDeleteAuthor = await AuthorizationService CanDeleteAuthor = await AuthorizationService
.IsGrantedAsync(BookStorePermissions.Authors.Delete); .IsGrantedAsync(BookStorePermissions.Authors.Delete);
} }
private async Task GetAuthorsAsync() private async Task GetAuthorsAsync()
{ {
var result = await AuthorAppService.GetListAsync( var result = await AuthorAppService.GetListAsync(
new GetAuthorListDto new GetAuthorListDto
{ {
MaxResultCount = PageSize, MaxResultCount = PageSize,
SkipCount = CurrentPage * PageSize, SkipCount = CurrentPage * PageSize,
Sorting = CurrentSorting Sorting = CurrentSorting
} }
); );
AuthorList = result.Items; AuthorList = result.Items;
TotalCount = (int)result.TotalCount; TotalCount = (int)result.TotalCount;
} }
private async Task OnDataGridReadAsync(DataGridReadDataEventArgs<AuthorDto> e) private async Task OnDataGridReadAsync(DataGridReadDataEventArgs<AuthorDto> e)
{ {
CurrentSorting = e.Columns CurrentSorting = e.Columns
.Where(c => c.SortDirection != SortDirection.Default) .Where(c => c.SortDirection != SortDirection.Default)
.Select(c => c.Field + (c.SortDirection == SortDirection.Descending ? " DESC" : "")) .Select(c => c.Field + (c.SortDirection == SortDirection.Descending ? " DESC" : ""))
.JoinAsString(","); .JoinAsString(",");
CurrentPage = e.Page - 1; CurrentPage = e.Page - 1;
await GetAuthorsAsync(); await GetAuthorsAsync();
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
private void OpenCreateAuthorModal() private void OpenCreateAuthorModal()
{ {
CreateValidationsRef.ClearAll(); CreateValidationsRef.ClearAll();
NewAuthor = new CreateAuthorDto(); NewAuthor = new CreateAuthorDto();
CreateAuthorModal.Show(); CreateAuthorModal.Show();
} }
private void CloseCreateAuthorModal() private void CloseCreateAuthorModal()
{ {
CreateAuthorModal.Hide(); CreateAuthorModal.Hide();
} }
private void OpenEditAuthorModal(AuthorDto author) private void OpenEditAuthorModal(AuthorDto author)
{ {
EditValidationsRef.ClearAll(); EditValidationsRef.ClearAll();
EditingAuthorId = author.Id; EditingAuthorId = author.Id;
EditingAuthor = ObjectMapper.Map<AuthorDto, UpdateAuthorDto>(author); EditingAuthor = ObjectMapper.Map<AuthorDto, UpdateAuthorDto>(author);
EditAuthorModal.Show(); EditAuthorModal.Show();
} }
private async Task DeleteAuthorAsync(AuthorDto author) private async Task DeleteAuthorAsync(AuthorDto author)
{
var confirmMessage = L["AuthorDeletionConfirmationMessage", author.Name];
if (!await Message.Confirm(confirmMessage))
{ {
var confirmMessage = L["AuthorDeletionConfirmationMessage", author.Name]; return;
if (!await Message.Confirm(confirmMessage))
{
return;
}
await AuthorAppService.DeleteAsync(author.Id);
await GetAuthorsAsync();
} }
private void CloseEditAuthorModal() await AuthorAppService.DeleteAsync(author.Id);
{ await GetAuthorsAsync();
EditAuthorModal.Hide(); }
}
private void CloseEditAuthorModal()
{
EditAuthorModal.Hide();
}
private async Task CreateAuthorAsync() private async Task CreateAuthorAsync()
{
if (await CreateValidationsRef.ValidateAll())
{ {
if (await CreateValidationsRef.ValidateAll()) await AuthorAppService.CreateAsync(NewAuthor);
{ await GetAuthorsAsync();
await AuthorAppService.CreateAsync(NewAuthor); CreateAuthorModal.Hide();
await GetAuthorsAsync();
CreateAuthorModal.Hide();
}
} }
}
private async Task UpdateAuthorAsync() private async Task UpdateAuthorAsync()
{
if (await EditValidationsRef.ValidateAll())
{ {
if (await EditValidationsRef.ValidateAll()) await AuthorAppService.UpdateAsync(EditingAuthorId, EditingAuthor);
{ await GetAuthorsAsync();
await AuthorAppService.UpdateAsync(EditingAuthorId, EditingAuthor); EditAuthorModal.Hide();
await GetAuthorsAsync();
EditAuthorModal.Hide();
}
} }
} }
} }
@ -1240,11 +1232,11 @@ We should complete the localizations we've used above. Open the `en.json` file u
Run and login to the application. **If you don't see the Authors menu item under the Book Store menu, that means you don't have the permission yet.** Go to the `identity/roles` page, click to the *Actions* button and select the *Permissions* action for the **admin role**: Run and login to the application. **If you don't see the Authors menu item under the Book Store menu, that means you don't have the permission yet.** Go to the `identity/roles` page, click to the *Actions* button and select the *Permissions* action for the **admin role**:
![bookstore-author-permissions](images/bookstore-author-permissions.png) ![bookstore-author-permissions](images/bookstore-author-permissions-2.png)
As you see, the admin role has no *Author Management* permissions yet. Click to the checkboxes and save the modal to grant the necessary permissions. You will see the *Authors* menu item under the *Book Store* in the main menu, after **refreshing the page**: As you see, the admin role has no *Author Management* permissions yet. Click to the checkboxes and save the modal to grant the necessary permissions. You will see the *Authors* menu item under the *Book Store* in the main menu, after **refreshing the page**:
![bookstore-authors-page](images/bookstore-authors-blazor-ui.png) ![bookstore-authors-page](images/bookstore-authors-page-3.png)
That's all! This is a fully working CRUD page, you can create, edit and delete the authors. That's all! This is a fully working CRUD page, you can create, edit and delete the authors.

BIN
docs/en/Tutorials/images/blazor-add-book-button-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
docs/en/Tutorials/images/blazor-bookstore-book-list-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

BIN
docs/en/Tutorials/images/blazor-bookstore-book-list-with-authors-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

BIN
docs/en/Tutorials/images/blazor-delete-book-action-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
docs/en/Tutorials/images/blazor-edit-book-action-3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
docs/en/Tutorials/images/blazor-edit-book-modal-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
docs/en/Tutorials/images/blazor-new-book-modal-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
docs/en/Tutorials/images/book-create-modal-with-author-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
docs/en/Tutorials/images/bookstore-actions-buttons-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
docs/en/Tutorials/images/bookstore-angular-author-selection-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
docs/en/Tutorials/images/bookstore-angular-authors-page-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

BIN
docs/en/Tutorials/images/bookstore-author-permissions-3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
docs/en/Tutorials/images/bookstore-authors-page-3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
docs/en/Tutorials/images/bookstore-book-list-angular.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

BIN
docs/en/Tutorials/images/bookstore-books-with-authorname-angular-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
docs/en/Tutorials/images/bookstore-confirmation-popup-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
docs/en/Tutorials/images/bookstore-empty-new-book-modal-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
docs/en/Tutorials/images/bookstore-final-actions-dropdown-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
docs/en/Tutorials/images/bookstore-new-book-form-v3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Loading…
Cancel
Save