@ -0,0 +1,690 @@
|
||||
# How to Design Multi-Lingual Entity
|
||||
|
||||
## Introduction
|
||||
|
||||
If you want to open up to the global market these days, end-to-end localization is a must. ABP provides an already established infrastructure for static texts. However, this may not be sufficient for many applications. You may need to fully customize your app for a particular language and region.
|
||||
|
||||
Let's take a look at a few quotes from Christian Arno's article "[How Foreign-Language Internet Strategies Boost Sales](https://www.mediapost.com/publications/article/155250/how-foreign-language-internet-strategies-boost-sal.html)" to better understand the impact of this:
|
||||
|
||||
- 82% of European consumers are less likely to buy online if the site is not in their native tongue ([Eurobarometer survey](http://europa.eu/rapid/pressReleasesAction.do?reference=IP/11/556)).
|
||||
- 72.4% of global consumers are more likely to buy a product if the information is available in their own language ([Common Sense Advisory](http://www.commonsenseadvisory.com/)).
|
||||
- The English language currently only accounts for 31% of all online use, and more than half of all searches are in languages other than English.
|
||||
- Today, 42% of all Internet users are in Asia, while almost one-quarter are in Europe and just over 10% are in Latin America.
|
||||
|
||||
- Foreign languages have experienced exponential growth in online usage in the past decade -- with Chinese now officially the [second-most-prominent-language](http://english.peopledaily.com.cn/90001/90776/90882/7438489.html) on the Web. [Arabic](http://www.internetworldstats.com/stats7.htm) has increased by a whopping 2500%, while English has only risen by 204%
|
||||
|
||||
If you are looking for ways to expand your market share by fully customizing your application for a particular language and region, in this article I will explain how you can do it with ABP framework.
|
||||
|
||||
### Source Code
|
||||
|
||||
You can find the source code of the application at [abpframework/abp-samples](https://github.com/abpframework/abp-samples/tree/master/AcmeBookStoreMultiLingual).
|
||||
|
||||
### Demo of the Final Application
|
||||
|
||||
At the end of this article, we will have created an application same as in the gif below.
|
||||
|
||||

|
||||
|
||||
## Development
|
||||
|
||||
In order to keep the article short and get rid of unrelated information in the article (like defining entities etc.), we'll be using the [BookStore](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) example, which is used in the "[Web Application Development Tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=MVC&DB=EF)" documentation of ABP Framework and we will make the Book entity as multi-lingual. If you do not want to finish this tutorial, you can find the application [here](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore).
|
||||
|
||||
### Determining the data model
|
||||
|
||||
We need a robust, maintainable, and efficient data model to store content in multiple languages.
|
||||
|
||||
> I read many articles to determine the data model correctly, and as a result, I decided to use one of the many approaches that suit us.
|
||||
> However, as in everything, there is a trade-off here. If you are wondering about the advantages and disadvantages of the model we will implement compared to other models, I recommend you to read [this article](https://vertabelo.com/blog/data-modeling-for-multiple-languages-how-to-design-a-localization-ready-system/).
|
||||
|
||||

|
||||
|
||||
As a result of the tutorial, we already have the `Book` and `Author` entities, as an extra, we will just add the `BookTranslation`.
|
||||
|
||||
> In the article, we will make the Name property of the Book entity multi-lingual, but the article is independent of the Book entity, you can make the entity you want multi-lingual with similar codes according to your requirements.
|
||||
|
||||
#### Acme.BookStore.Domain.Shared
|
||||
|
||||
Create a folder named `MultiLingualObjects` and create the following interfaces in its contents.
|
||||
|
||||
We will use the `IObjectTranslation` interface to mark the translation of a multi-lingual entity:
|
||||
|
||||
```csharp
|
||||
public interface IObjectTranslation
|
||||
{
|
||||
string Language { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
We will use the `IMultiLingualObject<TTranslation>` interface to mark multi-lingual entities:
|
||||
|
||||
```csharp
|
||||
public interface IMultiLingualObject<TTranslation>
|
||||
where TTranslation : class, IObjectTranslation
|
||||
{
|
||||
ICollection<TTranslation> Translations { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
#### Acme.BookStore.Domain
|
||||
|
||||
In the `Books` folder, create the `BookTranslation` class as follows:
|
||||
|
||||
```csharp
|
||||
public class BookTranslation : Entity, IObjectTranslation
|
||||
{
|
||||
public Guid BookId { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public string Language { get; set; }
|
||||
|
||||
public override object[] GetKeys()
|
||||
{
|
||||
return new object[] {BookId, Language};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`BookTranslation` contains the `Language` property, which contains a language code for translation and a reference to the multi-lingual entity. We also have the `BookId` foreign key to help us know which book is translated.
|
||||
|
||||
Implement `IMultiLingualObject` in the `Book` class as follows:
|
||||
|
||||
```csharp
|
||||
public class Book : AuditedAggregateRoot<Guid>, IMultiLingualObject<BookTranslation>
|
||||
{
|
||||
public Guid AuthorId { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public BookType Type { get; set; }
|
||||
|
||||
public DateTime PublishDate { get; set; }
|
||||
|
||||
public float Price { get; set; }
|
||||
|
||||
public ICollection<BookTranslation> Translations { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
Create a folder named `MultiLingualObjects` and add the following class inside of this folder:
|
||||
|
||||
```csharp
|
||||
public class MultiLingualObjectManager : ITransientDependency
|
||||
{
|
||||
protected const int MaxCultureFallbackDepth = 5;
|
||||
|
||||
public async Task<TTranslation> FindTranslationAsync<TMultiLingual, TTranslation>(
|
||||
TMultiLingual multiLingual,
|
||||
string culture = null,
|
||||
bool fallbackToParentCultures = true)
|
||||
where TMultiLingual : IMultiLingualObject<TTranslation>
|
||||
where TTranslation : class, IObjectTranslation
|
||||
{
|
||||
culture ??= CultureInfo.CurrentUICulture.Name;
|
||||
|
||||
if (multiLingual.Translations.IsNullOrEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var translation = multiLingual.Translations.FirstOrDefault(pt => pt.Language == culture);
|
||||
if (translation != null)
|
||||
{
|
||||
return translation;
|
||||
}
|
||||
|
||||
if (fallbackToParentCultures)
|
||||
{
|
||||
translation = GetTranslationBasedOnCulturalRecursive(
|
||||
CultureInfo.CurrentUICulture.Parent,
|
||||
multiLingual.Translations,
|
||||
0
|
||||
);
|
||||
|
||||
if (translation != null)
|
||||
{
|
||||
return translation;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected TTranslation GetTranslationBasedOnCulturalRecursive<TTranslation>(
|
||||
CultureInfo culture, ICollection<TTranslation> translations, int currentDepth)
|
||||
where TTranslation : class, IObjectTranslation
|
||||
{
|
||||
if (culture == null ||
|
||||
culture.Name.IsNullOrWhiteSpace() ||
|
||||
translations.IsNullOrEmpty() ||
|
||||
currentDepth > MaxCultureFallbackDepth)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var translation = translations.FirstOrDefault(pt => pt.Language.Equals(culture.Name, StringComparison.OrdinalIgnoreCase));
|
||||
return translation ?? GetTranslationBasedOnCulturalRecursive(culture.Parent, translations, currentDepth + 1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
With `MultiLingualObjectManager`'s `FindTranslationAsync` method, we get the translated version of the book according to `CurrentUICulture`. If no translation of culture is found, we return null.
|
||||
|
||||
> Every thread in .NET has `CurrentCulture` and `CurrentUICulture` objects.
|
||||
|
||||
#### Acme.BookStore.EntityFrameworkCore
|
||||
|
||||
In the `OnModelCreating` method of the `BookStoreDbContext` class, configure the `BookTranslation` as follows:
|
||||
|
||||
```csharp
|
||||
builder.Entity<BookTranslation>(b =>
|
||||
{
|
||||
b.ToTable(BookStoreConsts.DbTablePrefix + "BookTranslations",
|
||||
BookStoreConsts.DbSchema);
|
||||
|
||||
b.ConfigureByConvention();
|
||||
|
||||
b.HasKey(x => new {x.BookId, x.Language});
|
||||
});
|
||||
```
|
||||
|
||||
> I haven't explicitly set up a one-to-many relationship between `Book` and `BookTranslation` here, but the entity framework will do it for us.
|
||||
|
||||
After that, you can just run the following command in a command-line terminal to add a new database migration (in the directory of the `EntityFrameworkCore` project):
|
||||
|
||||
```bash
|
||||
dotnet ef migrations add Added_BookTranslation
|
||||
```
|
||||
|
||||
This will add a new migration class to your project. You can then run the following command (or run the `.DbMigrator` application) to apply changes to the database:
|
||||
|
||||
```bash
|
||||
dotnet ef database update
|
||||
```
|
||||
|
||||
Add the following code to the `ConfigureServices` method of the `BookStoreEntityFrameworkCoreModule`:
|
||||
|
||||
```csharp
|
||||
Configure<AbpEntityOptions>(options =>
|
||||
{
|
||||
options.Entity<Book>(bookOptions =>
|
||||
{
|
||||
bookOptions.DefaultWithDetailsFunc = query => query.Include(o => o.Translations);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Now we can use `WithDetailsAsync` without any parameters on `BookAppService` knowing that `Translations` will be included.
|
||||
|
||||
#### Acme.BookStore.Application.Contracts
|
||||
|
||||
Implement `IObjectTranslation` in the `BookDto` class as follows:
|
||||
|
||||
```csharp
|
||||
public class BookDto : AuditedEntityDto<Guid>, IObjectTranslation
|
||||
{
|
||||
public Guid AuthorId { get; set; }
|
||||
|
||||
public string AuthorName { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public BookType Type { get; set; }
|
||||
|
||||
public DateTime PublishDate { get; set; }
|
||||
|
||||
public float Price { get; set; }
|
||||
|
||||
public string Language { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
`Language` property is required to understand which language the translated book name belongs to in the UI.
|
||||
|
||||
Create the `AddBookTranslationDto` class in the `Books` folder as follows:
|
||||
|
||||
```csharp
|
||||
public class AddBookTranslationDto : IObjectTranslation
|
||||
{
|
||||
[Required]
|
||||
public string Language { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
Add the `AddTranslationsAsync` method to the `IBookAppService` as follows:
|
||||
|
||||
```csharp
|
||||
public interface IBookAppService :
|
||||
ICrudAppService<
|
||||
BookDto,
|
||||
Guid,
|
||||
PagedAndSortedResultRequestDto,
|
||||
CreateUpdateBookDto>
|
||||
{
|
||||
Task<ListResultDto<AuthorLookupDto>> GetAuthorLookupAsync();
|
||||
|
||||
Task AddTranslationsAsync(Guid id, AddBookTranslationDto input); // added this line
|
||||
}
|
||||
```
|
||||
|
||||
#### Acme.BookStore.Application
|
||||
|
||||
Now, we need to implement the `AddTranslationsAsync` method in `BookAppService` and include `Translations` in the `Book` entity, for this you can change the `BookAppService` as follows:
|
||||
|
||||
```csharp
|
||||
[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
|
||||
{
|
||||
private readonly IAuthorRepository _authorRepository;
|
||||
|
||||
public BookAppService(
|
||||
IRepository<Book, Guid> repository,
|
||||
IAuthorRepository authorRepository)
|
||||
: base(repository)
|
||||
{
|
||||
_authorRepository = authorRepository;
|
||||
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)
|
||||
{
|
||||
//Get the IQueryable<Book> from the repository
|
||||
var queryable = await Repository.WithDetailsAsync(); // this line changed
|
||||
|
||||
//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);
|
||||
bookDto.AuthorName = queryResult.author.Name;
|
||||
return bookDto;
|
||||
}
|
||||
|
||||
public override async Task<PagedResultDto<BookDto>> GetListAsync(PagedAndSortedResultRequestDto input)
|
||||
{
|
||||
//Get the IQueryable<Book> from the repository
|
||||
var queryable = await Repository.WithDetailsAsync(); // this line changed
|
||||
|
||||
//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
|
||||
select new {book, author};
|
||||
|
||||
//Paging
|
||||
query = query
|
||||
.OrderBy(NormalizeSorting(input.Sorting))
|
||||
.Skip(input.SkipCount)
|
||||
.Take(input.MaxResultCount);
|
||||
|
||||
//Execute the query and get a list
|
||||
var queryResult = await AsyncExecuter.ToListAsync(query);
|
||||
|
||||
//Convert the query result to a list of BookDto objects
|
||||
var bookDtos = queryResult.Select(x =>
|
||||
{
|
||||
var bookDto = ObjectMapper.Map<Book, BookDto>(x.book);
|
||||
bookDto.AuthorName = x.author.Name;
|
||||
return bookDto;
|
||||
}).ToList();
|
||||
|
||||
//Get the total count with another query
|
||||
var totalCount = await Repository.GetCountAsync();
|
||||
|
||||
return new PagedResultDto<BookDto>(
|
||||
totalCount,
|
||||
bookDtos
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<ListResultDto<AuthorLookupDto>> GetAuthorLookupAsync()
|
||||
{
|
||||
var authors = await _authorRepository.GetListAsync();
|
||||
|
||||
return new ListResultDto<AuthorLookupDto>(
|
||||
ObjectMapper.Map<List<Author>, List<AuthorLookupDto>>(authors)
|
||||
);
|
||||
}
|
||||
|
||||
public async Task AddTranslationsAsync(Guid id, AddBookTranslationDto input)
|
||||
{
|
||||
var queryable = await Repository.WithDetailsAsync();
|
||||
|
||||
var book = await AsyncExecuter.FirstOrDefaultAsync(queryable, x => x.Id == id);
|
||||
|
||||
if (book.Translations.Any(x => x.Language == input.Language))
|
||||
{
|
||||
throw new UserFriendlyException($"Translation already available for {input.Language}");
|
||||
}
|
||||
|
||||
book.Translations.Add(new BookTranslation
|
||||
{
|
||||
BookId = book.Id,
|
||||
Name = input.Name,
|
||||
Language = input.Language
|
||||
});
|
||||
|
||||
await Repository.UpdateAsync(book);
|
||||
}
|
||||
|
||||
private static string NormalizeSorting(string sorting)
|
||||
{
|
||||
if (sorting.IsNullOrEmpty())
|
||||
{
|
||||
return $"book.{nameof(Book.Name)}";
|
||||
}
|
||||
|
||||
if (sorting.Contains("authorName", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return sorting.Replace(
|
||||
"authorName",
|
||||
"author.Name",
|
||||
StringComparison.OrdinalIgnoreCase
|
||||
);
|
||||
}
|
||||
|
||||
return $"book.{sorting}";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Create the `MultiLingualBookObjectMapper` class as follows:
|
||||
|
||||
```csharp
|
||||
public class MultiLingualBookObjectMapper : IObjectMapper<Book, BookDto>, ITransientDependency
|
||||
{
|
||||
private readonly MultiLingualObjectManager _multiLingualObjectManager;
|
||||
|
||||
private readonly ISettingProvider _settingProvider;
|
||||
|
||||
public MultiLingualBookObjectMapper(
|
||||
MultiLingualObjectManager multiLingualObjectManager,
|
||||
ISettingProvider settingProvider)
|
||||
{
|
||||
_multiLingualObjectManager = multiLingualObjectManager;
|
||||
_settingProvider = settingProvider;
|
||||
}
|
||||
|
||||
public BookDto Map(Book source)
|
||||
{
|
||||
var translation = AsyncHelper.RunSync(() =>
|
||||
_multiLingualObjectManager.FindTranslationAsync<Book, BookTranslation>(source));
|
||||
|
||||
return new BookDto
|
||||
{
|
||||
Id = source.Id,
|
||||
AuthorId = source.AuthorId,
|
||||
Type = source.Type,
|
||||
Name = translation?.Name ?? source.Name,
|
||||
PublishDate = source.PublishDate,
|
||||
Price = source.Price,
|
||||
Language = translation?.Language ?? AsyncHelper.RunSync(() => _settingProvider.GetOrNullAsync(LocalizationSettingNames.DefaultLanguage)),
|
||||
CreationTime = source.CreationTime,
|
||||
CreatorId = source.CreatorId,
|
||||
LastModificationTime = source.LastModificationTime,
|
||||
LastModifierId = source.LastModifierId
|
||||
};
|
||||
}
|
||||
|
||||
public BookDto Map(Book source, BookDto destination)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To map the multi-lingual `Book` entity to `BookDto`, we implement custom mapping using the `IObjectMapper<TSource, TDestination>` interface. If no translation is found, default values are returned.
|
||||
|
||||
So far we have created the entire infrastructure. We don't need to change anything in the UI, if there is a translation according to the language chosen by the user, the list view will change. However, I want to create a simple modal where we can add new translations to an existing book in order to see what we have done.
|
||||
|
||||
#### Acme.BookStore.Web
|
||||
|
||||
Create a new razor page named `AddTranslationModal` in the `Books` folder as below.
|
||||
|
||||
**View**
|
||||
|
||||
```html
|
||||
@page
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
|
||||
@model Acme.BookStore.Web.Pages.Books.AddTranslationModal
|
||||
|
||||
@{
|
||||
Layout = null;
|
||||
}
|
||||
|
||||
<form asp-page="/Books/AddTranslationModal">
|
||||
<abp-modal>
|
||||
<abp-modal-header>Translations</abp-modal-header>
|
||||
<abp-modal-body>
|
||||
<abp-input asp-for="Id"></abp-input>
|
||||
<abp-select asp-for="@Model.TranslationViewModel.Language" asp-items="Model.Languages" class="form-select">
|
||||
<option selected value="">Pick a language</option>
|
||||
</abp-select>
|
||||
<abp-input asp-for="TranslationViewModel.Name"></abp-input>
|
||||
</abp-modal-body>
|
||||
<abp-modal-footer buttons="@(AbpModalButtons.Cancel | AbpModalButtons.Save)"></abp-modal-footer>
|
||||
</abp-modal>
|
||||
</form>
|
||||
```
|
||||
|
||||
**Model**
|
||||
|
||||
```csharp
|
||||
public class AddTranslationModal : BookStorePageModel
|
||||
{
|
||||
[HiddenInput]
|
||||
[BindProperty(SupportsGet = true)]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public List<SelectListItem> Languages { get; set; }
|
||||
|
||||
[BindProperty]
|
||||
public BookTranslationViewModel TranslationViewModel { get; set; }
|
||||
|
||||
private readonly IBookAppService _bookAppService;
|
||||
private readonly ILanguageProvider _languageProvider;
|
||||
|
||||
public AddTranslationModal(
|
||||
IBookAppService bookAppService,
|
||||
ILanguageProvider languageProvider)
|
||||
{
|
||||
_bookAppService = bookAppService;
|
||||
_languageProvider = languageProvider;
|
||||
}
|
||||
|
||||
public async Task OnGetAsync()
|
||||
{
|
||||
Languages = await GetLanguagesSelectItem();
|
||||
|
||||
TranslationViewModel = new BookTranslationViewModel();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> OnPostAsync()
|
||||
{
|
||||
await _bookAppService.AddTranslationsAsync(Id, ObjectMapper.Map<BookTranslationViewModel, AddBookTranslationDto>(TranslationViewModel));
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private async Task<List<SelectListItem>> GetLanguagesSelectItem()
|
||||
{
|
||||
var result = await _languageProvider.GetLanguagesAsync();
|
||||
|
||||
return result.Select(
|
||||
languageInfo => new SelectListItem
|
||||
{
|
||||
Value = languageInfo.CultureName,
|
||||
Text = languageInfo.DisplayName + " (" + languageInfo.CultureName + ")"
|
||||
}
|
||||
).ToList();
|
||||
}
|
||||
|
||||
public class BookTranslationViewModel
|
||||
{
|
||||
[Required]
|
||||
[SelectItems(nameof(Languages))]
|
||||
public string Language { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Name { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then, we can open the `BookStoreWebAutoMapperProfile` class and define the required mapping as follows:
|
||||
|
||||
```csharp
|
||||
CreateMap<AddTranslationModal.BookTranslationViewModel, AddBookTranslationDto>();
|
||||
```
|
||||
|
||||
Finally, change the content of `index.js` in the `Books` folder as follows:
|
||||
|
||||
```javascript
|
||||
$(function () {
|
||||
var l = abp.localization.getResource('BookStore');
|
||||
var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal');
|
||||
var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal');
|
||||
var addTranslationModal = new abp.ModalManager(abp.appPath + 'Books/AddTranslationModal'); // added this line
|
||||
|
||||
var dataTable = $('#BooksTable').DataTable(
|
||||
abp.libs.datatables.normalizeConfiguration({
|
||||
serverSide: true,
|
||||
paging: true,
|
||||
order: [[1, "asc"]],
|
||||
searching: false,
|
||||
scrollX: true,
|
||||
ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList),
|
||||
columnDefs: [
|
||||
{
|
||||
title: l('Actions'),
|
||||
rowAction: {
|
||||
items:
|
||||
[
|
||||
{
|
||||
text: l('Edit'),
|
||||
visible: abp.auth.isGranted('BookStore.Books.Edit'),
|
||||
action: function (data) {
|
||||
editModal.open({ id: data.record.id });
|
||||
}
|
||||
},
|
||||
{
|
||||
text: l('Add Translation'), // added this action
|
||||
visible: abp.auth.isGranted('BookStore.Books.Edit'),
|
||||
action: function (data) {
|
||||
addTranslationModal.open({ id: data.record.id });
|
||||
}
|
||||
},
|
||||
{
|
||||
text: l('Delete'),
|
||||
visible: abp.auth.isGranted('BookStore.Books.Delete'),
|
||||
confirmMessage: function (data) {
|
||||
return l(
|
||||
'BookDeletionConfirmationMessage',
|
||||
data.record.name
|
||||
);
|
||||
},
|
||||
action: function (data) {
|
||||
acme.bookStore.books.book
|
||||
.delete(data.record.id)
|
||||
.then(function() {
|
||||
abp.notify.info(
|
||||
l('SuccessfullyDeleted')
|
||||
);
|
||||
dataTable.ajax.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
title: l('Name'),
|
||||
data: "name"
|
||||
},
|
||||
{
|
||||
title: l('Author'),
|
||||
data: "authorName"
|
||||
},
|
||||
{
|
||||
title: l('Type'),
|
||||
data: "type",
|
||||
render: function (data) {
|
||||
return l('Enum:BookType:' + data);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: l('PublishDate'),
|
||||
data: "publishDate",
|
||||
render: function (data) {
|
||||
return luxon
|
||||
.DateTime
|
||||
.fromISO(data, {
|
||||
locale: abp.localization.currentCulture.name
|
||||
}).toLocaleString();
|
||||
}
|
||||
},
|
||||
{
|
||||
title: l('Price'),
|
||||
data: "price"
|
||||
},
|
||||
{
|
||||
title: l('CreationTime'),
|
||||
data: "creationTime",
|
||||
render: function (data) {
|
||||
return luxon
|
||||
.DateTime
|
||||
.fromISO(data, {
|
||||
locale: abp.localization.currentCulture.name
|
||||
}).toLocaleString(luxon.DateTime.DATETIME_SHORT);
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
);
|
||||
|
||||
createModal.onResult(function () {
|
||||
dataTable.ajax.reload();
|
||||
});
|
||||
|
||||
editModal.onResult(function () {
|
||||
dataTable.ajax.reload();
|
||||
});
|
||||
|
||||
$('#NewBookButton').click(function (e) {
|
||||
e.preventDefault();
|
||||
createModal.open();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
With a multi-lingual application, you can expand your market share, but if not designed well, may your application will be unusable. So, I've tried to explain how to design a sustainable multi-lingual entity in this article.
|
||||
|
||||
### Source Code
|
||||
|
||||
You can find source code of the example solution used in this article [here](https://github.com/abpframework/abp-samples/tree/master/AcmeBookStoreMultiLingual).
|
||||
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 2.7 MiB |
@ -0,0 +1,154 @@
|
||||
# How to Add Custom Properties to the User Entity
|
||||
|
||||
> **Note:** If your application is less than version 4.4.x, please follow [this article](../2020-10-08-How-To-Add-Custom-Property-To-The-User-Entity/How-To-Add-Custom-Property-To-The-User-Entity.md).
|
||||
|
||||
## Introduction
|
||||
|
||||
In this step-by-step article, I will explain how you can customize the user entity class, which is available in every web application you create using the ABP framework, according to your needs. When you read this article, you will learn how to override the services of built-in modules, extend the entities, extend data transfer objects and customize the user interface in the applications you develop using the ABP framework.
|
||||
|
||||
> **Note:** This article is not about customizing the `Login` page. If you have such a need, please follow [this article](../2020-05-09-Customize-the-Login-Page-for-MVC-Razor-Page-Applications/POST.md).
|
||||
|
||||
You can see the screenshots below which we will reach at the end of the article.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Preparing the Project
|
||||
|
||||
### Startup template and the initial run
|
||||
|
||||
Abp Framework offers startup templates to get into the work faster. We can create a new startup template using Abp CLI:
|
||||
|
||||
`abp new CustomizeUserDemo`
|
||||
|
||||
> In this article, I will go through the MVC application, but it will work also in the [Angular](https://docs.abp.io/en/abp/latest/Getting-Started?UI=NG&DB=EF&Tiered=No), [Blazor Server](https://docs.abp.io/en/abp/latest/Getting-Started?UI=BlazorServer&DB=EF&Tiered=No), and [Blazor WebAssembly](https://docs.abp.io/en/abp/latest/Getting-Started?UI=Blazor&DB=EF&Tiered=No) application.
|
||||
|
||||
After the download is finished, we can run **CustomizeUserDemo.DbMigrator** project to create the database migrations and seed the initial data (admin user, role, etc). Then we can run `CustomizeUserDemo.Web` to see that our application is working.
|
||||
|
||||
> Default admin username is **admin** and password is **1q2w3E\***
|
||||
|
||||

|
||||
|
||||
In this article, we will go through a scenario together and find the solutions to our questions through this scenario. However, since the scenario is not a real-life scenario, it may be strange, please don't get too about this issue :)
|
||||
|
||||
## Step-1
|
||||
|
||||
Create the Users folder in the **CustomizeUserDemo.Domain.Shared** project, create the class `UserConsts` inside the folder and update the class you created as below:
|
||||
|
||||
```csharp
|
||||
public static class UserConsts
|
||||
{
|
||||
public const string TitlePropertyName = "Title";
|
||||
|
||||
public const string ReputationPropertyName = "Reputation";
|
||||
|
||||
public const int MaxTitleLength = 64;
|
||||
|
||||
public const double MaxReputationValue = 1_000;
|
||||
|
||||
public const double MinReputationValue = 1;
|
||||
}
|
||||
```
|
||||
|
||||
## Step-2
|
||||
|
||||
Update the `CustomizeUserDemoEfCoreEntityExtensionMappings` class in the **CustomizeUserDemo.EntityFramework** project in the EntityFrameworkCore folder as below:
|
||||
|
||||
```csharp
|
||||
public static class CustomizeUserDemoEfCoreEntityExtensionMappings
|
||||
{
|
||||
private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner();
|
||||
|
||||
public static void Configure()
|
||||
{
|
||||
CustomizeUserDemoGlobalFeatureConfigurator.Configure();
|
||||
CustomizeUserDemoModuleExtensionConfigurator.Configure();
|
||||
|
||||
OneTimeRunner.Run(() =>
|
||||
{
|
||||
ObjectExtensionManager.Instance
|
||||
.MapEfCoreProperty<IdentityUser, string>(
|
||||
UserConsts.TitlePropertyName,
|
||||
(_, propertyBuilder) =>
|
||||
{
|
||||
propertyBuilder.HasDefaultValue("");
|
||||
propertyBuilder.HasMaxLength(UserConsts.MaxTitleLength);
|
||||
}
|
||||
).MapEfCoreProperty<IdentityUser, int>(
|
||||
UserConsts.ReputationPropertyName,
|
||||
(_, propertyBuilder) =>
|
||||
{
|
||||
propertyBuilder.HasDefaultValue(UserConsts.MinReputationValue);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This class can be used to map these extra properties to table fields in the database. Please read [this](https://docs.abp.io/en/abp/latest/Customizing-Application-Modules-Extending-Entities) article to improve your understanding of what we are doing.
|
||||
|
||||
So far, we have added our extra features to the `User` entity and matched these features with the `ef core`.
|
||||
|
||||
Now we need to add migration to see what has changed in our database. This for, open the Package Manager Console (PMC) under the menu Tools > NuGet Package Manager.
|
||||
|
||||

|
||||
|
||||
Select the **CustomizeUserDemo.EntityFramework** as the **default project** and execute the following command:
|
||||
|
||||
```bash
|
||||
Add-Migration "Updated-User-Entity"
|
||||
```
|
||||
|
||||

|
||||
|
||||
This will create a new migration class inside the `Migrations` folder of the **CustomizeUserDemo.EntityFrameworkCore** project.
|
||||
|
||||
> If you are using another IDE than the Visual Studio, you can use `dotnet-ef` tool as [documented here](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli#create-a-migration).
|
||||
|
||||
Finally, run the **CustomizeUserDemo.DbMigrator** project to update the database.
|
||||
|
||||
When we updated the database, you can see that the `Title` and `Reputation` columns are added to the `Users` table.
|
||||
|
||||

|
||||
|
||||
## Step-3
|
||||
Open the `CustomizeUserDemoModuleExtensionConfigurator` in the **CustomizeUserDemo.Domain.Shared** project, and change the contents of the `ConfigureExtraProperties` method as shown below:
|
||||
```csharp
|
||||
private static void ConfigureExtraProperties()
|
||||
{
|
||||
ObjectExtensionManager.Instance.Modules().ConfigureIdentity(identity =>
|
||||
{
|
||||
identity.ConfigureUser(user =>
|
||||
{
|
||||
user.AddOrUpdateProperty<string>(
|
||||
UserConsts.TitlePropertyName,
|
||||
options =>
|
||||
{
|
||||
options.Attributes.Add(new RequiredAttribute());
|
||||
options.Attributes.Add(
|
||||
new StringLengthAttribute(UserConsts.MaxTitleLength)
|
||||
);
|
||||
}
|
||||
);
|
||||
user.AddOrUpdateProperty<int>(
|
||||
UserConsts.ReputationPropertyName,
|
||||
options =>
|
||||
{
|
||||
options.DefaultValue = UserConsts.MinReputationValue;
|
||||
options.Attributes.Add(
|
||||
new RangeAttribute(UserConsts.MinReputationValue, UserConsts.MaxReputationValue)
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
That's it. Now let's run the application and look at the Identity user page. You can also try to edit and recreate a record if you want, it will work even though we haven't done anything extra. Here is the magic code behind ABP framework.
|
||||
|
||||
If there is a situation you want to add, you can click the contribute button or make a comment. Also, if you like the article, don't forget to share it :)
|
||||
|
||||
Happy coding :)
|
||||
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 193 KiB |
@ -0,0 +1,452 @@
|
||||
# Deploying ABP Project to Azure App Service
|
||||
|
||||
In this document, you will learn how to create and deploy your first ABP web app to [Azure App Service](https://docs.microsoft.com/en-us/azure/app-service/overview). The App Service supports various versions of .NET apps, and provides a highly scalable, self-patching web hosting service. ABP web apps are cross-platform and can be hosted on Linux, Windows or MacOS.
|
||||
|
||||
****Prerequisites****
|
||||
|
||||
- An Azure account with an active subscription. [Create an account for free](https://azure.microsoft.com/free/dotnet).
|
||||
- A GitHub account [Create an account for free](http://github.com/).
|
||||
|
||||
|
||||
|
||||
## Creating a new ABP application
|
||||
|
||||
Create a repository on [GitHub.com](https://github.com/) (keep all settings as default).
|
||||
|
||||
Open the command prompt and clone the repository into a folder on your computer
|
||||
|
||||
```bash
|
||||
git clone https://github.com/your-username/your-repository-name.git
|
||||
```
|
||||
|
||||
Check your dotnet version. It should be at least 3.1.x
|
||||
|
||||
```bash
|
||||
dotnet --version
|
||||
```
|
||||
|
||||
Install or update the [ABP CLI](https://docs.abp.io/en/abp/latest/cli) with the following command:
|
||||
|
||||
```bash
|
||||
dotnet tool install -g Volo.Abp.Cli || dotnet tool update -g Volo.Abp.Cli
|
||||
```
|
||||
|
||||
Open the command prompt in the *GitHub repository folder* and create a new ABP Blazor solution with the command below:
|
||||
|
||||
```bash
|
||||
abp new YourAppName -u blazor
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Running the application
|
||||
|
||||
Open the command prompt in the *[YourAppName].DbMigrator* project and enter the command below to apply the database migrations:
|
||||
|
||||
```bash
|
||||
dotnet run
|
||||
```
|
||||
|
||||
Open the command prompt in the *[YourAppName].HttpApi.Host* project to run the API project:
|
||||
|
||||
```bash
|
||||
dotnet run
|
||||
```
|
||||
|
||||
Navigate to the *applicationUrl* specified in *the launchSettings.json* file of the *[YourAppName].HttpApi.Host project*. You should get the *Swagger window*
|
||||
|
||||
Open the command prompt in the *[YourAppName].Blazor* folder and enter the command below to run the Blazor project:
|
||||
|
||||
```bash
|
||||
dotnet run
|
||||
```
|
||||
|
||||
Navigate to the *applicationUrl* specified in the *launchSettings.json* file of the *[YourAppName].Blazor* project and you should see the landing page.
|
||||
|
||||
Stop both the *API* and the *Blazor* project by pressing **CTRL+C**
|
||||
|
||||
|
||||
|
||||
## Committing to GitHub
|
||||
|
||||
Before the GitHub commit, you have to delete the line "**/wwwroot/libs/*" at *.gitignore* file.
|
||||
|
||||

|
||||
|
||||
Open the command prompt in the root folder of your project and *add, commit and push* all your changes to your GitHub repository:
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m initialcommit
|
||||
git push
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Configuring Azure database connection string
|
||||
|
||||
Create a SQL database on Azure and change the connection string in all the *appsettings.json* files.
|
||||
|
||||
* Login into [Azure Portal](https://portal.azure.com/)
|
||||
|
||||
* Click **Create a resource**
|
||||
|
||||
* Search for *SQL Database*
|
||||
|
||||
* Click the **Create** button in the *SQL Database window*
|
||||
|
||||
* Create a new resource group. Name it *rg[YourAppName]*
|
||||
|
||||
* Enter *[YourAppName]Db* as database name
|
||||
|
||||
* Create a new Server and name it *[yourappname]server*
|
||||
|
||||
* Enter a serveradmin login and passwords. Click the **OK** button
|
||||
|
||||
* Select your *Location*
|
||||
|
||||
* Check *Allow Azure services to access server*
|
||||
|
||||
* Click **Configure database**. Go to the *Basic* version and click the **Apply** button
|
||||
|
||||
* Click the **Review + create** button. Click **Create**
|
||||
|
||||
* Click **Go to resource** and click **SQL server** when the SQL Database is created
|
||||
|
||||
* Click **Networking** under Security left side menu
|
||||
|
||||
* Select **Selected networks** and click **Add your client IP$ address** at the Firewall rules
|
||||
|
||||
* Select **Allow Azure and resources to access this seerver** and save
|
||||
|
||||
* Go to your **SQL database**, click **Connection strings** and copy the connection string
|
||||
|
||||
* Copy/paste the *appsettings.json* files of the *[YourAppName].HttpApi.Host* and the *[YourAppName].DbMigrator* project
|
||||
|
||||
* Do not forget to replace {your_password} with the correct server password you entered in Azure SQL Database
|
||||
|
||||
|
||||
|
||||
## Running DB Migrations
|
||||
|
||||
Open the command prompt in the *[YourAppName].DbMigrator* project again and enter the command below to apply the database migrations:
|
||||
|
||||
```bash
|
||||
dotnet run
|
||||
```
|
||||
|
||||
Open the command prompt in the *[YourAppName].HttpApi.Host* project and enter the command below to check your API is working:
|
||||
|
||||
```bash
|
||||
dotnet run
|
||||
```
|
||||
|
||||
Stop the *[YourAppName].HttpApi.Host* by pressing CTRL+C.
|
||||
|
||||
|
||||
|
||||
## Committing to GitHub
|
||||
|
||||
Open the command prompt in the root folder of your project and add, commit and push all your changes to your GitHub repository
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m initialcommit
|
||||
git push
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Setting up the Build pipeline in AzureDevops and publish the Build Artifacts
|
||||
|
||||
* Sign in Azure DevOps
|
||||
|
||||
* Click **New organization** and follow the steps to create a new organisation. Name it [YourAppName]org
|
||||
|
||||
* Enter [YourAppName]Proj as project name in the ***Create a project to get started*** window
|
||||
|
||||
* Select **Public visibility** and click the **Create project** button
|
||||
|
||||
* Click the **Pipelines** button to continue
|
||||
|
||||
* Click the **Create Pipeline** button
|
||||
|
||||
Select GitHub in the Select your repository window
|
||||
|
||||

|
||||
|
||||
* Enter the Connection name. *[YourAppName]GitHubConnection* and click **Authorize using OAuth**
|
||||
|
||||
* Select your **GitHub** [YourAppName]repo and click Continue
|
||||
|
||||
* Search for **ASP.NET** in the ***Select a template*** window
|
||||
|
||||

|
||||
|
||||
* Select the ASP.NET Core template and click the **Apply** button
|
||||
|
||||
* Add the below commands block as a first step in the pipeline
|
||||
|
||||
```
|
||||
- task: UseDotNet@2
|
||||
inputs:
|
||||
packageType: 'sdk'
|
||||
version: '6.0.106'
|
||||
```
|
||||
|
||||

|
||||
|
||||
* Select **Settings** on the second task(Nugetcommand@2) in the pipeline
|
||||
|
||||
* Select **Feeds in my Nuget.config** and type **Nuget.config** in the text box
|
||||
|
||||

|
||||
|
||||
* Add the below commands block to the end of the pipeline
|
||||
|
||||
```
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: 'Publish Artifact'
|
||||
inputs:
|
||||
PathtoPublish: '$(build.artifactstagingdirectory)'
|
||||
ArtifactName: '$(Parameters.ArtifactName)'
|
||||
condition: succeededOrFailed()
|
||||
```
|
||||
|
||||

|
||||
|
||||
```
|
||||
# ASP.NET
|
||||
# Build and test ASP.NET projects.
|
||||
# Add steps that publish symbols, save build artifacts, deploy, and more:
|
||||
# https://docs.microsoft.com/azure/devops/pipelines/apps/aspnet/build-aspnet-4
|
||||
|
||||
trigger:
|
||||
- main
|
||||
|
||||
pool:
|
||||
vmImage: 'windows-latest'
|
||||
|
||||
variables:
|
||||
solution: '**/*.sln'
|
||||
buildPlatform: 'Any CPU'
|
||||
buildConfiguration: 'Release'
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
inputs:
|
||||
packageType: 'sdk'
|
||||
version: '6.0.106'
|
||||
|
||||
- task: NuGetToolInstaller@1
|
||||
|
||||
- task: NuGetCommand@2
|
||||
inputs:
|
||||
command: 'restore'
|
||||
restoreSolution: '$(solution)'
|
||||
feedsToUse: 'config'
|
||||
nugetConfigPath: 'NuGet.config'
|
||||
|
||||
- task: VSBuild@1
|
||||
inputs:
|
||||
solution: '$(solution)'
|
||||
msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:PackageLocation="$(build.artifactStagingDirectory)"'
|
||||
platform: '$(buildPlatform)'
|
||||
configuration: '$(buildConfiguration)'
|
||||
|
||||
- task: VSTest@2
|
||||
inputs:
|
||||
platform: '$(buildPlatform)'
|
||||
configuration: '$(buildConfiguration)'
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: 'Publish Artifact'
|
||||
inputs:
|
||||
PathtoPublish: '$(build.artifactstagingdirectory)'
|
||||
ArtifactName: '$(Parameters.ArtifactName)'
|
||||
publishLocation: 'Container'
|
||||
condition: succeededOrFailed()
|
||||
```
|
||||
|
||||
* Click **Save & queue** in the top menu. Click **Save & queue** again and click **Save and run** to run the Build pipeline
|
||||
|
||||
* When the Build pipeline has finished. Click **1 published; 1 consumed**
|
||||
|
||||
|
||||
|
||||
## Creating a Web App in the Azure Portal to deploy [YourAppName].HttpApi.Host project
|
||||
|
||||
* Search for Web App in the *Search the Marketplace* field
|
||||
|
||||
* Click the **Create** button in the Web App window
|
||||
|
||||
* Select rg[YourAppName] in the *Resource Group* dropdown
|
||||
|
||||
* Enter [YourAppName]API in the *Name input* field
|
||||
|
||||
* Select code, .NET Core 3.1 (LTS) and windows as *Operating System*
|
||||
|
||||
* Enter [YourAppName]API in the *Name input* field
|
||||
|
||||
* Select .NET Core 3.1 (LTS) in the *Runtime stack* dropdown
|
||||
|
||||
* Select Windows as *Operating System*
|
||||
|
||||
* Select the same *Region* as in the SQL server you created in Part 3
|
||||
|
||||

|
||||
|
||||
* Click **Create new** in the Windows Plan. Name it [YourAppName]ApiWinPlan
|
||||
|
||||
* Click **Change size** in Sku and size. Go to the Dev/Test Free F1 version and click the **Apply** button
|
||||
|
||||

|
||||
|
||||
* Click the **Review + create** button. Click the **Create** button
|
||||
|
||||
* Click **Go to resource** when the Web App has been created
|
||||
|
||||
|
||||
|
||||
## Creating a release pipeline in the AzureDevops and deploy [YourAppName].HttpApi.Host project
|
||||
|
||||
* Sign in into [Azure DevOps](https://azure.microsoft.com/en-us/services/devops/)
|
||||
|
||||
* Click [YourAppName]Proj and click **Releases** in the *Pipelines* menu
|
||||
|
||||
* Click the **New pipeline** button in the *No release pipelines found* window
|
||||
|
||||
* Select *Azure App Service deployment* and click the **Apply** button
|
||||
|
||||

|
||||
|
||||
* Enter *[YourAppName]staging* in the *Stage name* field in the *Stage* window. And close the window
|
||||
|
||||
* Click **+ Add an artifact** in the *Pipeline* tab
|
||||
|
||||
* Select the **Build** icon as *Source type* in the *Add an artifact* window
|
||||
|
||||
* Select Build pipeline in the *Source (build pipeline)* dropdown and click the **Add** button
|
||||
|
||||

|
||||
|
||||
* Click the **Continuous deployment trigger (thunderbolt icon)**
|
||||
|
||||
* Set the toggle to **Enabled** in the the *Continuous deployment trigger* window
|
||||
|
||||
* Click **+ Add** in *No filters added*. Select **Include** in the *Type* dropdown. Select your branch in the *Build branch* dropdown and close the window
|
||||
|
||||

|
||||
|
||||
* Click **the little red circle with the exclamation mark** in the *Tasks* tab menu
|
||||
|
||||
* Select your subscription in the *Azure subscription* dropdown.
|
||||
|
||||

|
||||
|
||||
* Click **Authorize** and enter your credentials in the next screens
|
||||
|
||||
* After Authorization, select the **[YourAppName]API** in the *App service name* dropdown
|
||||
|
||||
* Click the **Deploy Azure App Service** task
|
||||
|
||||
* Select **[YourAppName].HttpApi.Host.zip** in the *Package or folder* input field
|
||||
|
||||

|
||||
|
||||
* Click the **Save** icon in the top menu and click **OK**
|
||||
|
||||
* Click **Create release** in the top menu. Click **Create** to create a release
|
||||
|
||||
* Click the *Pipeline* tab and wait until the Deployment succeeds
|
||||
|
||||

|
||||
|
||||
* Open a browser and navigate to the URL of your Web App
|
||||
|
||||
```
|
||||
https://[YourAppName]api.azurewebsites.net
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## Creating a Web App in Azure Portal to deploy [YourAppName].Blazor project
|
||||
|
||||
* Login into [Azure Portal](https://portal.azure.com/)
|
||||
|
||||
* Click **Create a resource**
|
||||
|
||||
* Search for *Web App* in the *Search the Marketplace* field
|
||||
|
||||
* Click the **Create** button in the *Web App* window
|
||||
|
||||
* Select *rg[YourAppName]* in the *Resource Group* dropdown
|
||||
|
||||
* Enter *[YourAppName]Blazor* in the *Name* input field
|
||||
|
||||
* Select *.NET Core 3.1 (LTS)* in the *Runtime stack* dropdown
|
||||
|
||||
* Select *Windows* as *Operating System*
|
||||
|
||||
* Select the same region as the SQL server you created in Part 3
|
||||
|
||||
* Select the [YourAppName]ApiWinPlan in the *Windows Plan* dropdown
|
||||
|
||||

|
||||
|
||||
* Click the **Review + create** button. Click **Create** button
|
||||
|
||||
* Click **Go to resource** when the Web App has been created
|
||||
|
||||
* Copy the URL of the Blazor Web App for later use
|
||||
|
||||
```
|
||||
https://[YourAppName]blazor.azurewebsites.net
|
||||
```
|
||||
|
||||
|
||||
## Changing the Web App configuration for the Azure App Service
|
||||
|
||||
Copy the URL of the Api Host and Blazor Web App. Change appsettings.json files in the Web App as follows images.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## Adding an extra Stage in the Release pipeline in the AzureDevops to deploy [YourAppName].Blazor project
|
||||
|
||||
* Go to the *Release* pipeline in [Azure DevOps](https://azure.microsoft.com/en-us/services/devops/) and click **Edit**
|
||||
|
||||
* Click the **+ Add** link and add a **New Stage**
|
||||
|
||||

|
||||
|
||||
* Select *Azure App Service deployment* and click the **Apply** button
|
||||
|
||||
* Enter *BlazorDeployment* in the *Stage name* input field and close the *Stage* window
|
||||
|
||||
* Click the **little red circle with the exclamation mark** in the BlazorDeployment stage
|
||||
|
||||
* Select your subscription in the *Azure subscription* dropdown
|
||||
|
||||
* Select your Blazor Web App in the *App service name* dropdown
|
||||
|
||||
* Click the **Deploy Azure App Service task**
|
||||
|
||||
* Select *[YourAppName].Blazor.zip* in the *Package or folder* input field
|
||||
|
||||

|
||||
|
||||
* Click **Save** in the top menu and click the **OK** button after
|
||||
|
||||
* Click **Create release** in the top menu and click the **Create** button
|
||||
|
||||

|
||||
|
||||

|
||||
@ -0,0 +1,140 @@
|
||||
# Empezando con ABP y una Aplicacion AspNet Core MVC Web
|
||||
|
||||
Este tutorial explica como empezar una aplicacion ABP desde cero usando las dependencias minimas. Uno generalmente desea
|
||||
empezar con la **[plantilla de inicio](Getting-Started-AspNetCore-MVC-Template.md)**.
|
||||
|
||||
## Crea un Proyecto Nuevo
|
||||
|
||||
1. Crea una Aplicacion Web AspNet Core nueva usando Visual Studio 2022 (17.0.0+):
|
||||
|
||||

|
||||
|
||||
2. Configura el nuevo proyecto:
|
||||
|
||||

|
||||
|
||||
3. Presione el boton Create:
|
||||
|
||||

|
||||
|
||||
## Instale el paquete Volo.Abp.AspNetCore.Mvc
|
||||
|
||||
Volo.Abp.AspNetCore.Mvc es el paquete de integracion con AspNet Core MVC para ABP. Siendo asi, instalalo en su proyecto:
|
||||
|
||||
````
|
||||
Install-Package Volo.Abp.AspNetCore.Mvc
|
||||
````
|
||||
|
||||
## Crea el primer modulo ABP
|
||||
|
||||
ABP es un marco de referencia modular y require una clase de **inicio (raíz) tipo modulo** derivada de ``AbpModule``:
|
||||
|
||||
````C#
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Volo.Abp;
|
||||
using Volo.Abp.AspNetCore.Mvc;
|
||||
using Volo.Abp.Modularity;
|
||||
|
||||
namespace BasicAspNetCoreApplication
|
||||
{
|
||||
[DependsOn(typeof(AbpAspNetCoreMvcModule))]
|
||||
public class AppModule : AbpModule
|
||||
{
|
||||
public override void OnApplicationInitialization(ApplicationInitializationContext context)
|
||||
{
|
||||
var app = context.GetApplicationBuilder();
|
||||
var env = context.GetEnvironment();
|
||||
|
||||
// Configura la canalización de peticiones HTTP.
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseExceptionHandler("/Error");
|
||||
// El valor por defecto de HSTS es 30 dias. Debes cambiar esto en ambientes productivos. Referencia https://aka.ms/aspnetcore-hsts.
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseStaticFiles();
|
||||
app.UseRouting();
|
||||
app.UseConfiguredEndpoints();
|
||||
}
|
||||
}
|
||||
}
|
||||
````
|
||||
|
||||
``AppModule`` es un buen nombre para el modulo de inicio de una aplicacion.
|
||||
|
||||
Los paquetes de ABP definen clases de tipo modulo y cada modulo puede depender de otro.
|
||||
En el codigo anterior, el ``AppModule`` depende de el modulo ``AbpAspNetCoreMvcModule`` (definido por el paquete [Volo.Abp.AspNetCore.Mvc](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc)). Es comun agregar el atributo ``DependsOn`` despues de instalar un paquete ABP nuevo.
|
||||
|
||||
En vez de la clase de inicion Startup, estamos configurando una canalizacion de ASP.NET Core en este modulo.
|
||||
|
||||
## La clase Program
|
||||
|
||||
El proximo paso es modificar la clase Program para integrate el sistema de modulos ABP:
|
||||
|
||||
````C#
|
||||
using BasicAspNetCoreApplication;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
await builder.Services.AddApplicationAsync<AppModule>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
await app.InitializeApplicationAsync();
|
||||
await app.RunAsync();
|
||||
````
|
||||
|
||||
``builder.Services.AddApplicationAsync<AppModule>();`` Agrega todos los servicios definidos en todos los modulos empezando desde ``AppModule``.
|
||||
|
||||
``app.InitializeApplicationAsync()`` inicializa y empieza la aplicacion.
|
||||
|
||||
## Ejecutar la Aplicación
|
||||
|
||||
Es todo! Ejecuta la aplicación, debe funcionar como esperado.
|
||||
|
||||
## Uso de Autofac como Marco de Inyección de Dependencia
|
||||
|
||||
Mientras el sistema de Inyección de Dependencia de ASP.NET Core es suficiente para requerimientos basico, [Autofac](https://autofac.org/) proporciona características avanzadas como Inyección de Propiedades e Intercepcion de Metodos, los cuales son necesarios para que ABP pueda llevar a cabo funciones avanzadas.
|
||||
|
||||
El acto de remplazar el sistema DI de ASP.NET Core por Autofac e integrarlo con ABP es facil.
|
||||
|
||||
1. Instala el paquete [Volo.Abp.Autofac](https://www.nuget.org/packages/Volo.Abp.Autofac)
|
||||
|
||||
````
|
||||
Install-Package Volo.Abp.Autofac
|
||||
````
|
||||
|
||||
2. Agrega la dependencia sobre el modulo ``AbpAutofacModule``
|
||||
|
||||
````C#
|
||||
[DependsOn(typeof(AbpAspNetCoreMvcModule))]
|
||||
[DependsOn(typeof(AbpAutofacModule))] //Agrega la dependencia sobre el modulo ABP Autofac
|
||||
public class AppModule : AbpModule
|
||||
{
|
||||
...
|
||||
}
|
||||
````
|
||||
|
||||
3. Actualiza `Program.cs` para que use Autofac:
|
||||
|
||||
````C#
|
||||
using BasicAspNetCoreApplication;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Host.UseAutofac(); //Agrega esta linea
|
||||
|
||||
await builder.Services.AddApplicationAsync<AppModule>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
await app.InitializeApplicationAsync();
|
||||
await app.RunAsync();
|
||||
````
|
||||
|
||||
## Codigo fuente
|
||||
|
||||
Obten el codigo fuente del ejemplo creado en este tutorial de [aqui](https://github.com/abpframework/abp-samples/tree/master/BasicAspNetCoreApplication).
|
||||
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 64 KiB |
@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Import Project="..\..\..\configureawait.props" />
|
||||
<Import Project="..\..\..\common.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace />
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Volo.Abp.AspNetCore.Mvc.Dapr\Volo.Abp.AspNetCore.Mvc.Dapr.csproj" />
|
||||
<ProjectReference Include="..\Volo.Abp.EventBus.Dapr\Volo.Abp.EventBus.Dapr.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Import Project="..\..\..\configureawait.props" />
|
||||
<Import Project="..\..\..\common.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace />
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Volo.Abp.AspNetCore.Mvc\Volo.Abp.AspNetCore.Mvc.csproj" />
|
||||
<ProjectReference Include="..\Volo.Abp.Dapr\Volo.Abp.Dapr.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapr.AspNetCore" Version="1.8.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -1,52 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Volo.Abp.DependencyInjection;
|
||||
|
||||
namespace Volo.Abp.AspNetCore.DependencyInjection;
|
||||
|
||||
[ExposeServices(
|
||||
typeof(IHybridServiceScopeFactory),
|
||||
typeof(HttpContextServiceScopeFactory)
|
||||
)]
|
||||
[Dependency(ReplaceServices = true)]
|
||||
public class HttpContextServiceScopeFactory : IHybridServiceScopeFactory, ITransientDependency
|
||||
{
|
||||
protected IHttpContextAccessor HttpContextAccessor { get; }
|
||||
|
||||
protected IServiceScopeFactory ServiceScopeFactory { get; }
|
||||
|
||||
public HttpContextServiceScopeFactory(
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IServiceScopeFactory serviceScopeFactory)
|
||||
{
|
||||
HttpContextAccessor = httpContextAccessor;
|
||||
ServiceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
|
||||
public virtual IServiceScope CreateScope()
|
||||
{
|
||||
var httpContext = HttpContextAccessor.HttpContext;
|
||||
if (httpContext == null)
|
||||
{
|
||||
return ServiceScopeFactory.CreateScope();
|
||||
}
|
||||
|
||||
return new NonDisposedHttpContextServiceScope(httpContext.RequestServices);
|
||||
}
|
||||
|
||||
protected class NonDisposedHttpContextServiceScope : IServiceScope
|
||||
{
|
||||
public IServiceProvider ServiceProvider { get; }
|
||||
|
||||
public NonDisposedHttpContextServiceScope(IServiceProvider serviceProvider)
|
||||
{
|
||||
ServiceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Volo.Abp.DependencyInjection;
|
||||
|
||||
[ExposeServices(
|
||||
typeof(IHybridServiceScopeFactory),
|
||||
typeof(DefaultServiceScopeFactory)
|
||||
)]
|
||||
public class DefaultServiceScopeFactory : IHybridServiceScopeFactory, ITransientDependency
|
||||
{
|
||||
protected IServiceScopeFactory Factory { get; }
|
||||
|
||||
public DefaultServiceScopeFactory(IServiceScopeFactory factory)
|
||||
{
|
||||
Factory = factory;
|
||||
}
|
||||
|
||||
public IServiceScope CreateScope()
|
||||
{
|
||||
return Factory.CreateScope();
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Volo.Abp.DependencyInjection;
|
||||
|
||||
public interface IHybridServiceScopeFactory : IServiceScopeFactory
|
||||
{
|
||||
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Import Project="..\..\..\configureawait.props" />
|
||||
<Import Project="..\..\..\common.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace />
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Volo.Abp.Json\Volo.Abp.Json.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapr.Client" Version="1.8.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -1,23 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Volo.Abp.Domain.Entities.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Used to pass data for an event when an entity (<see cref="IEntity"/>) is being changed (creating, updating or deleting).
|
||||
/// See <see cref="EntityCreatingEventData{TEntity}"/>, <see cref="EntityDeletingEventData{TEntity}"/> and <see cref="EntityUpdatingEventData{TEntity}"/> classes.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">Entity type</typeparam>
|
||||
[Serializable]
|
||||
[Obsolete("This event is no longer needed and identical to EntityChangedEventData. Please use EntityChangedEventData instead.")]
|
||||
public class EntityChangingEventData<TEntity> : EntityEventData<TEntity>
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
/// <param name="entity">Changing entity in this event</param>
|
||||
public EntityChangingEventData(TEntity entity)
|
||||
: base(entity)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Volo.Abp.Domain.Entities.Events;
|
||||
|
||||
/// <summary>
|
||||
/// This type of event is used to notify just before creation of an Entity.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">Entity type</typeparam>
|
||||
[Serializable]
|
||||
[Obsolete("This event is no longer needed and identical to EntityCreatedEventData. Please use EntityCreatedEventData instead.")]
|
||||
public class EntityCreatingEventData<TEntity> : EntityChangingEventData<TEntity>
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity which is being created</param>
|
||||
public EntityCreatingEventData(TEntity entity)
|
||||
: base(entity)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Volo.Abp.Domain.Entities.Events;
|
||||
|
||||
/// <summary>
|
||||
/// This type of event is used to notify just before deletion of an Entity.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">Entity type</typeparam>
|
||||
[Serializable]
|
||||
[Obsolete("This event is no longer needed and identical to EntityDeletedEventData. Please use EntityDeletedEventData instead.")]
|
||||
public class EntityDeletingEventData<TEntity> : EntityChangingEventData<TEntity>
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity which is being deleted</param>
|
||||
public EntityDeletingEventData(TEntity entity)
|
||||
: base(entity)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Volo.Abp.Domain.Entities.Events;
|
||||
|
||||
/// <summary>
|
||||
/// This type of event is used to notify just before update of an Entity.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">Entity type</typeparam>
|
||||
[Serializable]
|
||||
[Obsolete("This event is no longer needed and identical to EntityUpdatedEventData. Please use EntityUpdatedEventData instead.")]
|
||||
public class EntityUpdatingEventData<TEntity> : EntityChangingEventData<TEntity>
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity which is being updated</param>
|
||||
public EntityUpdatingEventData(TEntity entity)
|
||||
: base(entity)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Import Project="..\..\..\configureawait.props" />
|
||||
<Import Project="..\..\..\common.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace />
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Volo.Abp.Dapr\Volo.Abp.Dapr.csproj" />
|
||||
<ProjectReference Include="..\Volo.Abp.DistributedLocking.Abstractions\Volo.Abp.DistributedLocking.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||