mirror of https://github.com/abpframework/abp.git
331 changed files with 2933 additions and 1128 deletions
@ -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,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 |
@ -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 |
|
||||
{ |
|
||||
|
|
||||
} |
|
||||
@ -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) |
|
||||
{ |
|
||||
|
|
||||
} |
|
||||
} |
|
||||
@ -1,71 +0,0 @@ |
|||||
using System; |
|
||||
using System.Threading.Tasks; |
|
||||
using Microsoft.Extensions.DependencyInjection; |
|
||||
using Shouldly; |
|
||||
using Volo.Abp.Modularity; |
|
||||
using Xunit; |
|
||||
|
|
||||
namespace Volo.Abp.DependencyInjection; |
|
||||
|
|
||||
public class HybridServiceScopeFactory_Tests |
|
||||
{ |
|
||||
[Fact] |
|
||||
public async Task Should_Use_Default_ServiceScopeFactory_By_Default_Async() |
|
||||
{ |
|
||||
using (var application = await AbpApplicationFactory.CreateAsync<IndependentEmptyModule>()) |
|
||||
{ |
|
||||
application.Services.AddType(typeof(MyServiceAsync)); |
|
||||
|
|
||||
await application.InitializeAsync(); |
|
||||
|
|
||||
var serviceScopeFactory = application.ServiceProvider.GetRequiredService<IHybridServiceScopeFactory>(); |
|
||||
|
|
||||
using (var scope = serviceScopeFactory.CreateScope()) |
|
||||
{ |
|
||||
scope.ServiceProvider.GetRequiredService<MyServiceAsync>(); |
|
||||
} |
|
||||
|
|
||||
MyServiceAsync.DisposeCount.ShouldBe(1); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_Use_Default_ServiceScopeFactory_By_Default() |
|
||||
{ |
|
||||
using (var application = AbpApplicationFactory.Create<IndependentEmptyModule>()) |
|
||||
{ |
|
||||
application.Services.AddType(typeof(MyService)); |
|
||||
|
|
||||
application.Initialize(); |
|
||||
|
|
||||
var serviceScopeFactory = application.ServiceProvider.GetRequiredService<IHybridServiceScopeFactory>(); |
|
||||
|
|
||||
using (var scope = serviceScopeFactory.CreateScope()) |
|
||||
{ |
|
||||
scope.ServiceProvider.GetRequiredService<MyService>(); |
|
||||
} |
|
||||
|
|
||||
MyService.DisposeCount.ShouldBe(1); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private class MyServiceAsync : ITransientDependency, IDisposable |
|
||||
{ |
|
||||
public static int DisposeCount { get; private set; } |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
++DisposeCount; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private class MyService : ITransientDependency, IDisposable |
|
||||
{ |
|
||||
public static int DisposeCount { get; private set; } |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
++DisposeCount; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,6 +1,6 @@ |
|||||
namespace Volo.Blogging |
namespace Volo.Blogging |
||||
{ |
{ |
||||
public static class BloggingDbProperties |
public static class AbpBloggingDbProperties |
||||
{ |
{ |
||||
/// <summary>
|
/// <summary>
|
||||
/// Default value: "Blg".
|
/// Default value: "Blg".
|
||||
@ -1,15 +1,40 @@ |
|||||
using System; |
using System; |
||||
using System.Collections.Generic; |
using System.Collections.Generic; |
||||
using System.Linq; |
using System.ComponentModel.DataAnnotations; |
||||
using System.Text; |
using Microsoft.Extensions.DependencyInjection; |
||||
using System.Threading.Tasks; |
using Microsoft.Extensions.Localization; |
||||
|
using Volo.Abp.Validation.Localization; |
||||
|
using Volo.CmsKit.Localization; |
||||
|
using Volo.CmsKit.Tags; |
||||
|
|
||||
namespace Volo.CmsKit.Admin.Tags; |
namespace Volo.CmsKit.Admin.Tags; |
||||
|
|
||||
[Serializable] |
[Serializable] |
||||
public class EntityTagSetDto |
public class EntityTagSetDto : IValidatableObject |
||||
{ |
{ |
||||
public string EntityId { get; set; } |
public string EntityId { get; set; } |
||||
|
|
||||
public string EntityType { get; set; } |
public string EntityType { get; set; } |
||||
|
|
||||
|
[Required] |
||||
public List<string> Tags { get; set; } |
public List<string> Tags { get; set; } |
||||
|
|
||||
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) |
||||
|
{ |
||||
|
var l = validationContext.GetRequiredService<IStringLocalizer<AbpValidationResource>>(); |
||||
|
|
||||
|
foreach (var tag in Tags) |
||||
|
{ |
||||
|
if (tag.Length > TagConsts.MaxNameLength) |
||||
|
{ |
||||
|
yield return new ValidationResult( |
||||
|
l[ |
||||
|
"ThisFieldMustBeAStringWithAMaximumLengthOf{0}", |
||||
|
TagConsts.MaxNameLength |
||||
|
], |
||||
|
new[] { nameof(Tags) } |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
} |
} |
||||
|
|||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue