@ -0,0 +1,29 @@ |
|||
name: Documentation Checks |
|||
|
|||
on: |
|||
push: |
|||
branches: |
|||
- dev |
|||
paths: |
|||
# This ensures the check will only be run when something changes in the docs content |
|||
- "docs/en/**/*" |
|||
pull_request: |
|||
branches: |
|||
- dev |
|||
paths: |
|||
- "docs/en/**/*" |
|||
jobs: |
|||
spellcheck: |
|||
name: "Docs: Spellcheck (En)" |
|||
runs-on: ubuntu-latest |
|||
steps: |
|||
- uses: actions/checkout@v2 |
|||
name: Check out the code |
|||
- uses: actions/setup-node@v1 |
|||
name: Setup node |
|||
with: |
|||
node-version: "16" |
|||
- run: npm install -g cspell |
|||
name: Install cSpell |
|||
- run: cspell --config ./cSpell.json "docs/en/**/*.md" --no-progress # Update for path to the markdown files |
|||
name: Run cSpell |
|||
@ -0,0 +1,7 @@ |
|||
{ |
|||
"culture": "hu", |
|||
"texts": { |
|||
"AbpTitle": "ABP Framework – Nyílt forráskódú webalkalmazás-keretrendszer", |
|||
"AbpDescription": "Az ABP egy nyílt forráskódú alkalmazáskeret, amely az AspNet Core alapú webalkalmazások fejlesztésére összpontosít. Ne ismételje magát, összpontosítson saját üzleti kódjára." |
|||
} |
|||
} |
|||
@ -1,5 +1,6 @@ |
|||
{ |
|||
"culture": "en", |
|||
"texts": { |
|||
"Buy": "Buy" |
|||
} |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
{ |
|||
"culture": "hu", |
|||
"texts": { |
|||
"FAQ": "GYIK" |
|||
} |
|||
} |
|||
@ -0,0 +1,151 @@ |
|||
{ |
|||
"version": "0.2", |
|||
"language": "en", |
|||
"words": [ |
|||
"ABP's", |
|||
"abpframework", |
|||
"Antiforgery", |
|||
"appsettings", |
|||
"aspnet", |
|||
"aspnetcore", |
|||
"Autofac", |
|||
"automagically", |
|||
"Blazor", |
|||
"CQRS", |
|||
"crossfade", |
|||
"Dapr", |
|||
"Datagrid's", |
|||
"Datatable", |
|||
"datepicker", |
|||
"dismissable", |
|||
"dockerized", |
|||
"entrypoints", |
|||
"findability", |
|||
"hoverable", |
|||
"Iddict", |
|||
"IntelliCode", |
|||
"Keysize", |
|||
"Linq", |
|||
"Microservices", |
|||
"middlewares", |
|||
"Minifier", |
|||
"multitenancy", |
|||
"multitenant", |
|||
"Navs", |
|||
"Newtonsoft", |
|||
"Npgsql", |
|||
"oidc", |
|||
"overridable", |
|||
"Parameterless", |
|||
"Passwordless", |
|||
"PKCE", |
|||
"preconfigured", |
|||
"proxying", |
|||
"redirections", |
|||
"scrollbars", |
|||
"signin", |
|||
"Templating", |
|||
"textboxes", |
|||
"toolset", |
|||
"unsubscription", |
|||
"Xunit" |
|||
], |
|||
"ignoreWords": [ |
|||
"Aliyun", |
|||
"Allibone", |
|||
"Blazorise", |
|||
"Boutwell", |
|||
"Cmskit", |
|||
"connectionstrings", |
|||
"Devart", |
|||
"Formik", |
|||
"Halil", |
|||
"Hanselman", |
|||
"hikalkan", |
|||
"Ibrahim", |
|||
"İbrahim", |
|||
"Kalkan", |
|||
"Kirti", |
|||
"Kommunity", |
|||
"Kulkarni", |
|||
"Luxon", |
|||
"malihu", |
|||
"Malik", |
|||
"Masis", |
|||
"Minio", |
|||
"NGXS", |
|||
"NSWAG", |
|||
"Scriban", |
|||
"Serilog", |
|||
"Shoudly", |
|||
"Shouldly", |
|||
"Sweetalert", |
|||
"Syncfusion", |
|||
"Telerik", |
|||
"Timeago", |
|||
"Toastr", |
|||
"Volo", |
|||
"Volosoft", |
|||
"Xeevis" |
|||
], |
|||
"patterns": [ |
|||
{ |
|||
"name": "Markdown links", |
|||
"pattern": "\\((.*)\\)", |
|||
"description": "" |
|||
}, |
|||
{ |
|||
"name": "Markdown code blocks", |
|||
"pattern": "/^(\\s*`{3,}).*[\\s\\S]*?^\\1/gmx", |
|||
"description": "Taken from the cSpell example at https://cspell.org/configuration/patterns/#verbose-regular-expressions" |
|||
}, |
|||
{ |
|||
"name": "Inline code blocks", |
|||
"pattern": "\\`([^\\`\\r\\n]+?)\\`", |
|||
"description": "https://stackoverflow.com/questions/41274241/how-to-capture-inline-markdown-code-but-not-a-markdown-code-fence-with-regex" |
|||
}, |
|||
{ |
|||
"name": "Link contents", |
|||
"pattern": "\\<a(.*)\\>", |
|||
"description": "" |
|||
}, |
|||
{ |
|||
"name": "Snippet references", |
|||
"pattern": "-- snippet:(.*)", |
|||
"description": "" |
|||
}, |
|||
{ |
|||
"name": "Snippet references 2", |
|||
"pattern": "\\<\\[sample:(.*)", |
|||
"description": "another kind of snippet reference" |
|||
}, |
|||
{ |
|||
"name": "Multi-line code blocks", |
|||
"pattern": "/^\\s*```[\\s\\S]*?^\\s*```/gm" |
|||
}, |
|||
{ |
|||
"name": "HTML Tags", |
|||
"pattern": "<[^>]*>", |
|||
"description": "Reference: https://stackoverflow.com/questions/11229831/regular-expression-to-remove-html-tags-from-a-string" |
|||
}, |
|||
{ |
|||
"name": "Markdown Image", |
|||
"pattern": "!\\[(.*)\\]\\((.*)\\)" |
|||
} |
|||
], |
|||
"ignoreRegExpList": [ |
|||
"Markdown links", |
|||
"Markdown code blocks", |
|||
"Inline code blocks", |
|||
"Link contents", |
|||
"Snippet references", |
|||
"Snippet references 2", |
|||
"Multi-line code blocks", |
|||
"HTML Tags", |
|||
"Markdown Image" |
|||
], |
|||
"ignorePaths": [ |
|||
"**/*Release/Post.md", |
|||
"**/*Preview/POST.md" |
|||
] |
|||
} |
|||
@ -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](https://community.abp.io/posts/how-to-add-custom-property-to-the-user-entity-6ggxiddr). |
|||
|
|||
## 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](https://community.abp.io/posts/how-to-customize-the-login-page-for-mvc-razor-page-applications-9a40f3cd). |
|||
|
|||
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,242 @@ |
|||
# Using gRPC with the ABP Framework |
|||
|
|||
[gRPC](https://grpc.io/) defines itself as an open source, language agnostic, universal, high-performance **Remote Procedure Call (RPC)** framework. |
|||
|
|||
In this article, I will show you how to create a gRPC service and consume it from a console application with the ABP Framework. While the client application is console in this article, it can easily be a service consuming another service in a microservice system. |
|||
|
|||
> **This article will be a step by step tutorial.** I wrote the article based on Microsoft's [Code-first gRPC services and clients with .NET](https://docs.microsoft.com/en-us/aspnet/core/grpc/code-first) document. You can read that document for more details about gRPC and the code-first approach. |
|||
|
|||
## Creating the Application |
|||
|
|||
Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed it yet: |
|||
|
|||
````bash |
|||
dotnet tool install -g Volo.Abp.Cli |
|||
```` |
|||
|
|||
or update to the latest version if you've already installed an old version: |
|||
|
|||
````bash |
|||
dotnet tool update Volo.Abp.Cli -g |
|||
```` |
|||
|
|||
Create an empty folder, open a command-line terminal and type the following command in the terminal window to create a new ABP solution using the ABP CLI: |
|||
|
|||
````bash |
|||
abp new ProductManagement -u blazor -t app --preview |
|||
```` |
|||
|
|||
I've created an application with the Blazor UI, but the UI is not important for this tutorial, you can select your favorite UI option. |
|||
|
|||
## Open the Solution |
|||
|
|||
Open the solution in your favorite IDE. I like [Rider](https://www.jetbrains.com/rider/), but Visual Studio, VS Code or any other IDE perfectly works. The following figure shows the solution structure in Rider: |
|||
|
|||
 |
|||
|
|||
Run the `ProductManagement.DbMigrator` project (a console application) to create the database and seed the initial data. |
|||
|
|||
## Defining the Service Contract |
|||
|
|||
We are starting by defining the service contract and DTO classes that will be shared between the server and the client applications. |
|||
|
|||
Create a `Products` folder in the `ProductManagement.Application.Contracts` project and add a new interface named `IProductAppService`: |
|||
|
|||
````csharp |
|||
using System.Collections.Generic; |
|||
using System.ServiceModel; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace ProductManagement.Products; |
|||
|
|||
[ServiceContract] |
|||
public interface IProductAppService : IApplicationService |
|||
{ |
|||
Task<List<ProductDto>> GetListAsync(); |
|||
} |
|||
```` |
|||
|
|||
Your IDE will complain about the `[ServiceContract]` attribute, but it is necessary for the contract-first gRPC library we will be using later. So, add the [System.ServiceModel.Primitives](https://www.nuget.org/packages/System.ServiceModel.Primitives) NuGet package to the `ProductManagement.Application.Contracts` project, and it should be fixed. You can simply edit the `ProductManagement.Application.Contracts.csproj` file and add the following line in an `ItemGroup` tag: |
|||
|
|||
````xml |
|||
<PackageReference Include="System.ServiceModel.Primitives" Version="4.7.0" /> |
|||
```` |
|||
|
|||
Or you can use your IDE to find and add that NuGet package, it is up to you. |
|||
|
|||
I've also used the `ProductDto` class, but haven't defined it yet. Create a new class in the same folder with the `IProductAppService` file: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Runtime.Serialization; |
|||
|
|||
namespace ProductManagement.Products; |
|||
|
|||
[DataContract] |
|||
public class ProductDto |
|||
{ |
|||
[DataMember(Order = 1)] |
|||
public Guid Id { get; set; } |
|||
|
|||
[DataMember(Order = 2)] |
|||
public string Name { get; set; } |
|||
} |
|||
```` |
|||
|
|||
The `[DataContract]` and `[DataMember]` properties are needed for serialization. In gRPC, property serialization orders are important, because property names are not transferred to the target application, to keep the serialized data small. |
|||
|
|||
After adding these classes, the `ProductManagement.Application.Contracts` project should look as in the following figure: |
|||
|
|||
 |
|||
|
|||
The contracts part is over. We actually didn't have any dependency to gRPC at that point. Our service and DTOs are pretty plain classes, except a few standard attributes, which are already defined in the .NET Core framework. Now, we can implement the `IProductAppService`. |
|||
|
|||
## Implementing the Service |
|||
|
|||
We are implementing the application services in the `ProductManagement.Application` project. So, add a new `Products` folder to that project and define a `ProductAppService` class inside it: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace ProductManagement.Products; |
|||
|
|||
public class ProductAppService : ProductManagementAppService, IProductAppService |
|||
{ |
|||
public async Task<List<ProductDto>> GetListAsync() |
|||
{ |
|||
return new List<ProductDto> |
|||
{ |
|||
new ProductDto { Id = Guid.NewGuid(), Name = "Product 1" }, |
|||
new ProductDto { Id = Guid.NewGuid(), Name = "Product 2" }, |
|||
}; |
|||
} |
|||
} |
|||
```` |
|||
|
|||
This is a pretty standard, plain [application service ](https://docs.abp.io/en/abp/latest/Application-Services)class. All the ABP application service features (validation, audit logging, unit of work, etc.) are available. You can inject [repositories](https://docs.abp.io/en/abp/latest/Repositories) and perform database queries. To keep this article simple, I am returning hard-coded data from here. |
|||
|
|||
> `ProductManagementAppService` is a base class coming in the ABP startup template. While you don't have to inherit from it, it provides useful base properties and methods you typically need in an application service. |
|||
|
|||
The application service part is over. Again, we didn't write any gRPC specific code. Don't worry, we will write in the next section. |
|||
|
|||
## Configuring the gRPC Server |
|||
|
|||
In this solution, `ProductManagement.HttpApi.Host` is the project that configures and runs the server-side application. So, we will make changes in that project. |
|||
|
|||
First, add the [protobuf-net.Grpc.AspNetCore](https://www.nuget.org/packages/protobuf-net.Grpc.AspNetCore) NuGet package to the `ProductManagement.HttpApi.Host` project: |
|||
|
|||
````xml |
|||
<PackageReference Include="protobuf-net.Grpc.AspNetCore" Version="1.0.177" /> |
|||
```` |
|||
|
|||
Then open the `ProductManagementHttpApiHostModule.cs` file, find the `ConfigureServices` method and add the following line into this method: |
|||
|
|||
````csharp |
|||
context.Services.AddCodeFirstGrpc(); |
|||
```` |
|||
|
|||
This will register code-first gRPC services to the [dependency injection](https://docs.abp.io/en/abp/latest/Dependency-Injection) system. Then find the `app.UseConfiguredEndpoints()` line in the `OnApplicationInitialization` method and change it as shown below: |
|||
|
|||
````csharp |
|||
app.UseConfiguredEndpoints(endpoints => |
|||
{ |
|||
endpoints.MapGrpcService<IProductAppService>(); |
|||
}); |
|||
```` |
|||
|
|||
We've configured the `IProductAppService` to handle gRPC requests to that service. The following figure shows the whole change done in the `ProductManagementHttpApiHostModule` class: |
|||
|
|||
 |
|||
|
|||
gRPC handles requests with the HTTP/2 protocol and should listen an endpoint other than the default HTTP endpoint used by the application. We can easily configure the Kestrel server to listen two endpoints, one for our HTTP APIs, the other one for gRPC services. Add the following configuration inside the `appsettings.json` file of the `ProductManagement.HttpApi.Host` project: |
|||
|
|||
````json |
|||
"Kestrel": { |
|||
"Endpoints": { |
|||
"Https": { |
|||
"Url": "https://localhost:44388", |
|||
"Protocols": "Http1AndHttp2" |
|||
}, |
|||
"gRPC": { |
|||
"Url": "https://localhost:10042", |
|||
"Protocols": "Http2" |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Note that `https://localhost:44388` may be different for your case, since ABP CLI assignes a random port while you're creating a new solution. You can check your port by running the `ProductManagement.HttpApi.Host` project and looking at the address bar on your browser. |
|||
|
|||
The server-side configuration is done. It is ready to receive gRPC requests. Now, we can change the client to consume the gRPC service we've created. |
|||
|
|||
## Implementing the Client Side |
|||
|
|||
The ABP startup solution template comes with a console application to test consuming your HTTP APIs. For this example, the project is named as `ProductManagement.HttpApi.Client.ConsoleTestApp` and located under the `test` folder in the solution. |
|||
|
|||
First, add the [Grpc.Net.Client](https://www.nuget.org/packages/Grpc.Net.Client) and the [protobuf-net.Grpc](https://www.nuget.org/packages/protobuf-net.Grpc) NuGet packages to the `ProductManagement.HttpApi.Client.ConsoleTestApp` project. |
|||
|
|||
````xml |
|||
<PackageReference Include="Grpc.Net.Client" Version="2.49.0-pre1" /> |
|||
<PackageReference Include="protobuf-net.Grpc" Version="1.0.177" /> |
|||
```` |
|||
|
|||
Now, open the `ClientDemoService.cs` file under the `ProductManagement.HttpApi.Client.ConsoleTestApp` project and change its contents with the following code block: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Grpc.Net.Client; |
|||
using ProductManagement.Products; |
|||
using ProtoBuf.Grpc.Client; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace ProductManagement.HttpApi.Client.ConsoleTestApp; |
|||
|
|||
public class ClientDemoService : ITransientDependency |
|||
{ |
|||
public async Task RunAsync() |
|||
{ |
|||
using (var channel = GrpcChannel.ForAddress("https://localhost:10042")) |
|||
{ |
|||
var productAppService = channel.CreateGrpcService<IProductAppService>(); |
|||
var productDtos = await productAppService.GetListAsync(); |
|||
|
|||
foreach (var productDto in productDtos) |
|||
{ |
|||
Console.WriteLine($"[Product] Id = {productDto.Id}, Name = {productDto.Name}"); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
We are simply creating a gRPC channel, then creating a client proxy for the `IProductAppService` service. Then we can call its method just like local method calls. You can run the applications to test it. |
|||
|
|||
## Run the Applications |
|||
|
|||
First run the `ProductManagement.HttpApi.Host` application. It should show a Swagger UI as shown below: |
|||
|
|||
 |
|||
|
|||
If you see that page, it means your server-side is up and running. Now, you can run the `ProductManagement.HttpApi.Client.ConsoleTestApp` console application to call the gRPC service defined on the server. |
|||
|
|||
The test console application should produce an output as shown below: |
|||
|
|||
 |
|||
|
|||
As you see, products are returned from the server. That's all, you've done it! |
|||
|
|||
## Conclusion |
|||
|
|||
In this article, I've used the [code-first approach](https://docs.microsoft.com/en-us/aspnet/core/grpc/code-first) to implement a gRPC server and consume it in a client application. Code-first approach is very practical if both of your client and server applications are built with .NET. By the help of ABP's layered solution structure, we even didn't add any gRPC dependency into our server-side and contracts. We've just configured gRPC in the hosting side, with a small amount of code. |
|||
|
|||
gRPC on .NET has different approaches, features, configurations and more details. I suggest you to read [Microsoft's documentation](https://docs.microsoft.com/en-us/aspnet/core/grpc) to learn more about it. All the approaches can work with the ABP Framework. Enjoy coding! |
|||
|
|||
## The Source Code |
|||
|
|||
* You can find the completed source code here: https://github.com/abpframework/abp-samples/tree/master/GrpcDemo2 |
|||
|
|||
* You can also see all the changes I've done in this article here: https://github.com/abpframework/abp-samples/pull/200/files |
|||
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 48 KiB |
@ -0,0 +1,121 @@ |
|||
# Consuming gRPC Services from Blazor WebAssembly Application Using gRPC-Web |
|||
|
|||
> **WARNING: I've demonstrated [Using gRPC with the ABP Framework](https://community.abp.io/posts/using-grpc-with-the-abp-framework-2dgaxzw3) in my latest post. If you haven't seen it, you should read it before this article, since this is a continuation of that article.** |
|||
|
|||
In this second part, I will show how to consume the gRPC service from the Blazor WebAssembly application, using the gRPC-Web technology. |
|||
|
|||
This will be a short article, based on Microsoft's [gRPC-Web in ASP.NET Core gRPC apps](https://learn.microsoft.com/en-us/aspnet/core/grpc/grpcweb) and [Code-first gRPC services and clients with .NET](https://learn.microsoft.com/en-us/aspnet/core/grpc/code-first) documents. For more information, I suggest to check these documents. Let's get started... |
|||
|
|||
## Configuring the Server Side |
|||
|
|||
First of all, the server-side should support gRPC-Web. Follow the steps below to enable it: |
|||
|
|||
### Add Grpc.AspNetCore.Web Package |
|||
|
|||
Add [Grpc.AspNetCore.Web](https://www.nuget.org/packages/Grpc.AspNetCore.Web) NuGet package to the `ProductManagement.HttpApi.Host` project. |
|||
|
|||
### Add GrpcWeb Middleware |
|||
|
|||
Add the following line just before the `app.UseConfiguredEndpoints(...)` line to add the GrpcWeb middleware to your ASP.NET Core request pipeline: |
|||
|
|||
````csharp |
|||
app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true }); |
|||
```` |
|||
|
|||
### Configure Cors |
|||
|
|||
ABP's startup template already configures Cors when you create a new solution. However, we need to allow some extra headers in our Cors configuration. |
|||
|
|||
Add the following line just after the `.WithAbpExposedHeaders()` line in the `OnApplicationInitialization` method of the `ProductManagementHttpApiHostModule` class: |
|||
|
|||
````csharp |
|||
.WithExposedHeaders("Grpc-Status", "Grpc-Message", "Grpc-Encoding", "Grpc-Accept-Encoding") |
|||
```` |
|||
|
|||
Finally, call `RequireCors` extension method just after the `MapGrpcService` calls: |
|||
|
|||
````csharp |
|||
app.UseConfiguredEndpoints(endpoints => |
|||
{ |
|||
endpoints |
|||
.MapGrpcService<IProductAppService>() |
|||
.RequireCors("__DefaultCorsPolicy"); // Configure Cors for the product service |
|||
}); |
|||
```` |
|||
|
|||
`__DefaultCorsPolicy` may seem a magic string here. Let me explain it: ABP startup template configures the default Cors policy with the `context.Services.AddCors(...)` method (you can see it in the source code). If we define a named policy, we should use the same name here. However, when we don't specify, ASP.NET Core uses `__DefaultCorsPolicy` as the policy name by default. If you don't want to use the magic string, you can resolve the `IOptions<CorsOptions>` service and get the `DefaultPolicyName` from the `CorsOption` object. |
|||
|
|||
Anyway, that's all on the server-side. We can work on he client now. |
|||
|
|||
## Configuring the Client Side |
|||
|
|||
`ProductManagement.Blazor` is the Blazor WebAssembly application in the solution I'd created in the [first article](https://community.abp.io/posts/using-grpc-with-the-abp-framework-2dgaxzw3). We will configure that project to be able to consume the server-side gRPC services from our Blazor application. |
|||
|
|||
### Add Client-side Nuget Packages |
|||
|
|||
Add [Grpc.Net.Client](https://www.nuget.org/packages/Grpc.Net.Client), [Grpc.Net.Client.Web](https://www.nuget.org/packages/Grpc.Net.Client.Web) and [protobuf-net.Grpc](https://www.nuget.org/packages/protobuf-net.Grpc) NuGet packages to the `ProductManagement.Blazor` project. We are ready to consume the gRPC services. |
|||
|
|||
### Consume the Product Service |
|||
|
|||
Change the `Pages/Index.razor.cs` file's content with the following code block: |
|||
|
|||
````csharp |
|||
using System.Collections.Generic; |
|||
using System.Net.Http; |
|||
using System.Threading.Tasks; |
|||
using Grpc.Net.Client; |
|||
using Grpc.Net.Client.Web; |
|||
using ProductManagement.Products; |
|||
using ProtoBuf.Grpc.Client; |
|||
|
|||
namespace ProductManagement.Blazor.Pages; |
|||
|
|||
public partial class Index |
|||
{ |
|||
private List<ProductDto> Products { get; set; } = new(); |
|||
|
|||
protected override async Task OnInitializedAsync() |
|||
{ |
|||
var channel = GrpcChannel.ForAddress("https://localhost:10042", new GrpcChannelOptions |
|||
{ |
|||
HttpHandler = new GrpcWebHandler(new HttpClientHandler()) |
|||
}); |
|||
|
|||
var productAppService = channel.CreateGrpcService<IProductAppService>(); |
|||
Products = await productAppService.GetListAsync(); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
* We've created a gRPC channel for the server-side endpoint (surely, you get the address from a configuration) with channel options by specifying that we will use the `GrpcWebHandler`. |
|||
* We've created a service proxy object using the `CreateGrpcService` extension method that is defined by the [protobuf-net.Grpc](https://www.nuget.org/packages/protobuf-net.Grpc) NuGet package. |
|||
* We've used the service proxy object, `productAppService`, to consume remote endpoint just like a local service. |
|||
|
|||
That's all. If we want to show the products on the page, we can add the following markup into the `Pages/Index.razor` view: |
|||
|
|||
````xml |
|||
<h2>A list of products:</h2> |
|||
|
|||
<ul class="list-group"> |
|||
@foreach(var product in Products) |
|||
{ |
|||
<li class="list-group-item"> |
|||
@product.Name <br/> |
|||
<small>@product.Id.ToString()</small> |
|||
</li> |
|||
} |
|||
</ul> |
|||
```` |
|||
|
|||
Run the applications (first run the `ProductManagement.HttpApi.Host` project, then run the `ProductManagement.Blazor` project in the solution) to see it in action: |
|||
|
|||
 |
|||
|
|||
## Conclusion |
|||
|
|||
In the first part of this article, I'd demonstrated how to implement a gRPC service and consume it in a client application, using the [code-first approach](https://docs.microsoft.com/en-us/aspnet/core/grpc/code-first). In this article, I've demonstrated how to consume the same gRPC service from a Blazor WebAssembly application, using the [gRPC-Web](https://learn.microsoft.com/en-us/aspnet/core/grpc/grpcweb) technology. As you see in these two articles, using gRPC with the ABP Framework is straightforward. |
|||
|
|||
## The Source Code |
|||
|
|||
- You can find the completed source code here: https://github.com/abpframework/abp-samples/tree/master/GrpcDemo2 |
|||
- You can also see all the changes I've done in this article here: https://github.com/abpframework/abp-samples/pull/201/files |
|||
|
After Width: | Height: | Size: 10 KiB |
@ -0,0 +1,265 @@ |
|||
# Upgrade Your Existing Projects to .NET7 |
|||
|
|||
A new .NET version has come. As open-source contributors, we are tracking the latest libraries and adopting them to our existing projects. In this sense, we completed our .NET 7 upgrade in our repositories for ABP Framework and ABP Commercial. In this article, I'll share the experiences we faced while upgrading to the new .NET version 👉 .NET 7. |
|||
|
|||
When I wrote this article, the latest .NET version was `7.0.0-rc.2`. So some of the version statements I wrote below must be changed due to the stable version release. |
|||
|
|||
|
|||
|
|||
**To see the latest and greatest stuff, let's see how to upgrade our existing projects to .NET 7!** |
|||
|
|||
|
|||
|
|||
## Install .NET7 SDK |
|||
|
|||
If you are on your development computer, then you need to install the .NET7 SDK `7.x.x`. For the production servers, you need to install the .NET 7 runtimes. Download link for the .NET7 SDK and runtimes is: |
|||
|
|||
* https://dotnet.microsoft.com/en-us/download/dotnet/7.0 |
|||
|
|||
|
|||
|
|||
## Update Your *.csproj Files |
|||
|
|||
First, you need to update all your `*.csproj` files to support .NET7. Find and replace all your `<TargetFramework>*</TargetFramework>` in the `*.csproj` files to support .NET 7: |
|||
|
|||
```xml |
|||
<TargetFramework>net7.0</TargetFramework> |
|||
``` |
|||
|
|||
We already did this in ABP Framework, see this commit as an example [github.com/abpframework/abp/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.csproj](https://github.com/abpframework/abp/blob/dev/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/MyCompanyName.MyProjectName.Mvc.csproj#L4). |
|||
|
|||
|
|||
|
|||
### Microsoft Package Updates |
|||
|
|||
You must be using Microsoft packages as well; then you need to update them to the latest .NET 7 version. |
|||
At the time, I wrote this article, the latest version was `7.0.0-rc.2.22476.2`, so I'll update them to this version including minor version changes. |
|||
|
|||
Here's the list of all package reference updates I did: |
|||
|
|||
```xml |
|||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.AspNetCore.Components" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="7.0.0-rc.2.*" /> |
|||
|
|||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0-rc.2.*"> |
|||
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.0-rc.2.*"> |
|||
|
|||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.Extensions.FileProviders.Composite" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.Extensions.Localization" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="7.0.0-rc.2.*" /> |
|||
|
|||
<PackageReference Include="System.Text.Encoding.CodePages" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="System.Text.Encodings.Web" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="System.Text.Json" Version="7.0.0-rc.2.*" /> |
|||
|
|||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="System.Collections.Immutable" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="7.0.0-rc.2.*" /> |
|||
<PackageReference Include="Microsoft.Data.Sqlite" Version="7.0.0-rc.2.*" /> |
|||
``` |
|||
|
|||
|
|||
|
|||
--- |
|||
|
|||
|
|||
|
|||
## Entity Framework Core Updates |
|||
|
|||
If you use EF Core as your data access library, you should update your `dotnet-ef` CLI tool. Here's the terminal command to update it: |
|||
|
|||
```bash |
|||
dotnet tool update dotnet-ef --global --prerelease |
|||
``` |
|||
|
|||
We already did the the EF Core package reference update in the *Microsoft Package Updates* section. |
|||
|
|||
|
|||
|
|||
### Breaking Change: OrderBy |
|||
|
|||
This release makes a breaking change in an EF Core query running behavior. We faced this issue in some of our queries that were missing `OrderBy` statement. It throws an exception and does not run the query. Here's the explanation of a EF Core team member for this issue: [github.com/dotnet/efcore/issues/21202#issuecomment-913206415](https://github.com/dotnet/efcore/issues/21202#issuecomment-913206415). |
|||
|
|||
The following exception is being thrown: |
|||
|
|||
> InvalidOperationException: The query uses 'Skip' without specifying ordering and uses split query mode. This generates incorrect results. Either provide ordering or run query in single query mode using AsSingleQuery(). See https://go.microsoft.com/fwlink/?linkid=2196526 for more information |
|||
|
|||
If you don't want to add `OrderBy` statement to solve the issue, you can also use `AsSingleQuery()`. |
|||
|
|||
 |
|||
|
|||
|
|||
|
|||
### EF Core - SQL Client Connection String Update |
|||
|
|||
With this version, the behavior of the SQL connection has been changed. There is a keyword in the SQL connection string called `TrustServerCertificate`. This keyword indicates whether the channel will be encrypted while bypassing walking the certificate chain to validate trust. |
|||
|
|||
> When `TrustServerCertificate` is set to `True`, the transport layer will use SSL to encrypt the channel and bypass walking the certificate chain to validate trust. If `TrustServerCertificate` is set to `true` and encryption is turned on, the encryption level specified on the server will be used even if `Encrypt` is set to `false`. The connection will fail otherwise. |
|||
|
|||
|
|||
|
|||
After the .NET7 update, it just started to throw the following exception: |
|||
|
|||
> A connection was successfully established with the server, but then an error occurred during the login process. |
|||
|
|||
|
|||
|
|||
We fixed this problem by adding the `TrustServerCertificate=true` to your connection string. Here's an example connection string that supports, |
|||
|
|||
````sql |
|||
Server=localhost; Database=MyProjectName; Trusted_Connection=True; TrustServerCertificate=True |
|||
```` |
|||
|
|||
See our commit for this fix: |
|||
|
|||
* [github.com/abpframework/abp/commit/96f17e67918eb87edd2baf876d4a7598281fe608](https://github.com/abpframework/abp/commit/96f17e67918eb87edd2baf876d4a7598281fe608) |
|||
|
|||
Related docs: |
|||
|
|||
* [learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/breaking-changes#encrypt-true](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/breaking-changes#encrypt-true) |
|||
* [stackoverflow.com/questions/34430550/a-connection-was-successfully-established-with-the-server-but-then-an-error-occ](https://stackoverflow.com/questions/34430550/a-connection-was-successfully-established-with-the-server-but-then-an-error-occ) |
|||
|
|||
|
|||
|
|||
--- |
|||
|
|||
|
|||
|
|||
## Blazor Update |
|||
|
|||
Ensure that you have updated your Blazor project's csproj to support .NET7: |
|||
|
|||
```xml |
|||
<TargetFramework>net7.0</TargetFramework> |
|||
``` |
|||
|
|||
### Install Blazor Workloads |
|||
|
|||
#### .NET Web Assembly build tools |
|||
|
|||
If you have a Blazor-WASM project, install the workloads by running the following in a command shell: |
|||
|
|||
```bash |
|||
dotnet workload install wasm-tools |
|||
``` |
|||
|
|||
**OR** you can update your workloads by running the following command in your Blazor project directory: |
|||
|
|||
```bash |
|||
dotnet workload restore |
|||
``` |
|||
|
|||
|
|||
|
|||
--- |
|||
|
|||
|
|||
|
|||
## .NET MAUI Update |
|||
|
|||
Ensure that you have updated your Blazor project's csproj to support .NET7: |
|||
|
|||
```xml |
|||
<TargetFrameworks>net7.0-android;net7.0-ios;net7.0-maccatalyst</TargetFrameworks> |
|||
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net7.0-windows10.0.19041.0</TargetFrameworks> |
|||
``` |
|||
|
|||
|
|||
|
|||
### Install MAUI Workloads |
|||
|
|||
If you have .NET MAUI project, then you also need to update your `TargetFramework` as below: |
|||
|
|||
If you have a .NET MAUI project, after installing the .NET 7 SDK, install the latest workloads with the following command: |
|||
|
|||
```bash |
|||
dotnet workload install maui |
|||
``` |
|||
|
|||
**OR** run the following command in your existing .NET MAUI project directory |
|||
|
|||
```bash |
|||
dotnet workload restore |
|||
``` |
|||
|
|||
|
|||
|
|||
Further information, check out https://github.com/dotnet/maui/wiki/.NET-7-and-.NET-MAUI |
|||
|
|||
--- |
|||
|
|||
|
|||
|
|||
#### Dotnet Maui Check Tool |
|||
|
|||
Alternatively, there's a 3rd party tool for .NET MAUI to install the required workloads. This tool installs the missing SDK packs. You can reach the tool's repository at [github.com/Redth/dotnet-maui-check](https://github.com/Redth/dotnet-maui-check). |
|||
|
|||
Installation: |
|||
|
|||
```bash |
|||
dotnet tool install -g Redth.Net.Maui.Check |
|||
``` |
|||
|
|||
Run: |
|||
|
|||
```bash |
|||
maui-check |
|||
``` |
|||
|
|||
|
|||
|
|||
--- |
|||
|
|||
|
|||
|
|||
## Docker Image Update |
|||
|
|||
If you are using Docker to automate the deployment of applications, you also need to update your images. We were using `aspnet:6.0.0-bullseye-slim` base and after the .NET 7 update, we started using `aspnet:7.0-bullseye-slim` in our Docker files. |
|||
|
|||
``` |
|||
FROM mcr.microsoft.com/dotnet/aspnet:7.0-bullseye-slim AS base |
|||
``` |
|||
|
|||
For this update, you can check out the following commit as an example: |
|||
|
|||
* [github.com/abpframework/abp/commit/2d07b9bd00152bef4658c48ff9b2cbee5788d308](https://github.com/abpframework/abp/commit/2d07b9bd00152bef4658c48ff9b2cbee5788d308) |
|||
|
|||
|
|||
|
|||
## ABP Framework .NET 7 Update |
|||
|
|||
In [ABP Framework repository](https://github.com/abpframework/abp), we pdated all our dependencies from .NET 6 to .NET 7. |
|||
Not all the changes are here, but you can check out the following PR of the .NET 7 update: |
|||
|
|||
* [github.com/abpframework/abp/pull/13626/files](https://github.com/abpframework/abp/pull/13626/files) |
|||
|
|||
|
|||
... |
|||
|
|||
Happy coding with .NET 7 🤗 |
|||
|
|||
... |
|||
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 133 KiB |
@ -0,0 +1,469 @@ |
|||
# ABP Dapr Integration |
|||
|
|||
> This document assumes that you are already familiar with [Dapr](https://dapr.io/) and you want to use it in your ABP based applications. |
|||
|
|||
[Dapr](https://dapr.io/) (Distributed Application Runtime) provides APIs that simplify microservice connectivity. It is an open source project that is mainly backed by Microsoft. It is also a CNCF (Cloud Native Computing Foundation) project and trusted by the community. |
|||
|
|||
ABP and Dapr have some intersecting features like service-to-service communication, distributed message bus and distributed locking. However their purposes are totally different. ABP's goal is to provide an end-to-end developer experience by offering an opinionated architecture and providing the necessary infrastructure libraries, reusable modules and tools to implement that architecture properly. Dapr's purpose, on the other hand, is to provide a runtime to decouple common microservice communication patterns from your application logic. |
|||
|
|||
ABP and Dapr can perfectly work together in the same application. ABP offers some packages to provide better integration where Dapr features intersect with ABP. You can use other Dapr features with no ABP integration packages based on [its own documentation](https://docs.dapr.io/). |
|||
|
|||
## ABP Dapr Integration Packages |
|||
|
|||
ABP provides the following NuGet packages for the Dapr integration: |
|||
|
|||
* [Volo.Abp.Dapr](https://www.nuget.org/packages/Volo.Abp.Dapr): The main Dapr integration package. All other packages depend on this package. |
|||
* [Volo.Abp.Http.Client.Dapr](https://www.nuget.org/packages/Volo.Abp.Http.Client.Dapr): Integration package for ABP's [dynamic](../API/Dynamic-CSharp-API-Clients.md) and [static](../API/Static-CSharp-API-Clients.md) C# API Client Proxies systems with Dapr's [service invocation](https://docs.dapr.io/developing-applications/building-blocks/service-invocation/service-invocation-overview/) building block. |
|||
* [Volo.Abp.EventBus.Dapr](https://www.nuget.org/packages/Volo.Abp.EventBus.Dapr): Implements ABP's distributed event bus with Dapr's [publish & subscribe](https://docs.dapr.io/developing-applications/building-blocks/pubsub/) building block. With this package, you can send events, but can not receive. |
|||
* [Volo.Abp.AspNetCore.Mvc.Dapr.EventBus](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus): Provides the endpoints to receive events from Dapr's [publish & subscribe](https://docs.dapr.io/developing-applications/building-blocks/pubsub/) building block. Use this package to send and receive events. |
|||
* [Volo.Abp.DistributedLocking.Dapr](https://www.nuget.org/packages/Volo.Abp.DistributedLocking.Dapr): Uses Dapr's [distributed lock](https://docs.dapr.io/developing-applications/building-blocks/distributed-lock/) building block for [distributed locking](../Distributed-Locking.md) service of the ABP Framework. |
|||
|
|||
In the following sections, we will see how to use these packages to use Dapr in your ABP based solutions. |
|||
|
|||
## Basics |
|||
|
|||
### Installation |
|||
|
|||
> This section explains how to add [Volo.Abp.Dapr](https://www.nuget.org/packages/Volo.Abp.Dapr), the core Dapr integration package to your project. If you are using one of the other Dapr integration packages, you can skip this section since this package will be indirectly added. |
|||
|
|||
Use the ABP CLI to add the [Volo.Abp.Dapr](https://www.nuget.org/packages/Volo.Abp.Dapr) NuGet package to your project: |
|||
|
|||
* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed it before. |
|||
* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.Dapr` package. |
|||
* Run the `abp add-package Volo.Abp.Dapr` command. |
|||
|
|||
If you want to do it manually, install the [Volo.Abp.Dapr](https://www.nuget.org/packages/Volo.Abp.Dapr) NuGet package to your project and add `[DependsOn(typeof(AbpDaprModule))]` to the [ABP module](../Module-Development-Basics.md) class inside your project. |
|||
|
|||
### AbpDaprOptions |
|||
|
|||
`AbpDaprOptions` is the main [options class](../Options.md) that you can configure the global Dapr settings with. **All settings are optional and you mostly don't need to configure them.** If you need, you can configure it in the `ConfigureServices` method of your [module class](../Module-Development-Basics.md): |
|||
|
|||
````csharp |
|||
Configure<AbpDaprOptions>(options => |
|||
{ |
|||
// ... |
|||
}); |
|||
```` |
|||
|
|||
Available properties of the `AbpDaprOptions` class: |
|||
|
|||
* `HttpEndpoint` (optional): HTTP endpoint that is used while creating a `DaprClient` object. If you don't specify, the default value is used. |
|||
* `GrpcEndpoint` (optional): The gRPC endpoint that is used while creating a `DaprClient` object. If you don't specify, the default value is used. |
|||
* `DaprApiToken` (optional): The [Dapr API token](https://docs.dapr.io/operations/security/api-token/) that is used while sending requests from the application to Dapr. It is filled from the `DAPR_API_TOKEN` environment variable by default (which is set by Dapr once it is configured). See the *Security* section in this document for details. |
|||
* `AppApiToken` (optional): The [App API token](https://docs.dapr.io/operations/security/app-api-token/) that is used to validate requests coming from Dapr. It is filled from the `APP_API_TOKEN` environment variable by default (which is set by Dapr once it is configured). See the *Security* section in this document for details. |
|||
|
|||
Alternatively, you can configure the options in the `Dapr` section of your `appsettings.json` file. Example: |
|||
|
|||
````csharp |
|||
"Dapr": { |
|||
"HttpEndpoint": "http://localhost:3500/" |
|||
} |
|||
```` |
|||
|
|||
### Injecting DaprClient |
|||
|
|||
ABP registers the `DaprClient` class to the [dependency injection](../Dependency-Injection.md) system. So, you can inject and use it whenever you need: |
|||
|
|||
````csharp |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly DaprClient _daprClient; |
|||
|
|||
public MyService(DaprClient daprClient) |
|||
{ |
|||
_daprClient = daprClient; |
|||
} |
|||
|
|||
public async Task DoItAsync() |
|||
{ |
|||
// TODO: Use the injected _daprClient object |
|||
} |
|||
} |
|||
```` |
|||
|
|||
Injecting `DaprClient` is the recommended way of using it in your application code. When you inject it, the `IAbpDaprClientFactory` service is used to create it, which is explained in the next section. |
|||
|
|||
### IAbpDaprClientFactory |
|||
|
|||
`IAbpDaprClientFactory` can be used to create `DaprClient` or `HttpClient` objects to perform operations on Dapr. It uses `AbpDaprOptions`, so you can configure the settings in a central place. |
|||
|
|||
**Example usages:** |
|||
|
|||
````csharp |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly IAbpDaprClientFactory _daprClientFactory; |
|||
|
|||
public MyService(IAbpDaprClientFactory daprClientFactory) |
|||
{ |
|||
_daprClientFactory = daprClientFactory; |
|||
} |
|||
|
|||
public async Task DoItAsync() |
|||
{ |
|||
// Create a DaprClient object with default options |
|||
DaprClient daprClient = await _daprClientFactory.CreateAsync(); |
|||
|
|||
/* Create a DaprClient object with configuring |
|||
* the DaprClientBuilder object */ |
|||
DaprClient daprClient2 = await _daprClientFactory |
|||
.CreateAsync(builder => |
|||
{ |
|||
builder.UseDaprApiToken("..."); |
|||
}); |
|||
|
|||
// Create an HttpClient object |
|||
HttpClient httpClient = await _daprClientFactory |
|||
.CreateHttpClientAsync("target-app-id"); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`CreateHttpClientAsync` method also gets optional `daprEndpoint` and `daprApiToken` parameters. |
|||
|
|||
> ABP uses `IAbpDaprClientFactory` when it needs to create a Dapr client. You can also use Dapr API to create client objects in your application. Using `IAbpDaprClientFactory` is recommended, but not required. |
|||
|
|||
## C# API Client Proxies Integration |
|||
|
|||
ABP can [dynamically](../API/Dynamic-CSharp-API-Clients.md) or [statically](../API/Static-CSharp-API-Clients.md) generate proxy classes to invoke your HTTP APIs from a Dotnet client application. It makes perfect sense to consume HTTP APIs in a distributed system. The [Volo.Abp.Http.Client.Dapr](https://www.nuget.org/packages/Volo.Abp.Http.Client.Dapr) package configures the client-side proxies system, so it uses Dapr's service invocation building block for the communication between your applications. |
|||
|
|||
### Installation |
|||
|
|||
Use the ABP CLI to add the [Volo.Abp.Http.Client.Dapr](https://www.nuget.org/packages/Volo.Abp.Http.Client.Dapr) NuGet package to your project (to the client side): |
|||
|
|||
* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed before. |
|||
* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.Http.Client.Dapr` package to. |
|||
* Run the `abp add-package Volo.Abp.Http.Client.Dapr` command. |
|||
|
|||
If you want to do it manually, install the [Volo.Abp.Http.Client.Dapr](https://www.nuget.org/packages/Volo.Abp.Http.Client.Dapr) NuGet package to your project and add `[DependsOn(typeof(AbpHttpClientDaprModule))]` to the [ABP module](../Module-Development-Basics.md) class inside your project. |
|||
|
|||
### Configuration |
|||
|
|||
Once you install the [Volo.Abp.Http.Client.Dapr](https://www.nuget.org/packages/Volo.Abp.Http.Client.Dapr) NuGet package, all you need to do is to configure ABP's remote services option either in `appsettings.json` or using the `AbpRemoteServiceOptions` [options class](../Options.md). |
|||
|
|||
**Example:** |
|||
|
|||
````csharp |
|||
{ |
|||
"RemoteServices": { |
|||
"Default": { |
|||
"BaseUrl": "http://dapr-httpapi/" |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`dapr-httpapi` in this example is the application id of the server application in your Dapr configuration. |
|||
|
|||
The remote service name (`Default` in this example) should match the remote service name specified in the `AddHttpClientProxies` call for dynamic client proxies or the `AddStaticHttpClientProxies` call for static client proxies. Using `Default` is fine if your client communicates to a single server. However, if your client uses multiple servers, you typically have multiple keys in the `RemoteServices` configuration. Once you configure the remote service endpoints as Dapr application ids, it will automatically work and make the HTTP calls through Dapr when you use ABP's client proxy system. |
|||
|
|||
> See the [dynamic](../API/Dynamic-CSharp-API-Clients.md) and [static](../API/Static-CSharp-API-Clients.md) client proxy documents for details about the ABP's client proxy system. |
|||
|
|||
## Distributed Event Bus Integration |
|||
|
|||
[ABP's distributed event bus](../Distributed-Event-Bus.md) system provides a convenient abstraction to allow applications to communicate asynchronously via events. ABP has integration packages with various distributed messaging systems, like RabbitMQ, Kafka, and Azure. Dapr also has a [publish & subscribe building block](https://docs.dapr.io/developing-applications/building-blocks/pubsub/pubsub-overview/) for the same purpose: distributed messaging / events. |
|||
|
|||
ABP's [Volo.Abp.EventBus.Dapr](https://www.nuget.org/packages/Volo.Abp.EventBus.Dapr) and [Volo.Abp.AspNetCore.Mvc.Dapr.EventBus](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus) packages make it possible to use the Dapr infrastructure for ABP's distributed event bus. |
|||
|
|||
The [Volo.Abp.EventBus.Dapr](https://www.nuget.org/packages/Volo.Abp.EventBus.Dapr) package can be used by any type of application (e.g., a Console or ASP.NET Core application) to publish events through Dapr. To be able to receive messages (by subscribing to events), you need to have the [Volo.Abp.AspNetCore.Mvc.Dapr.EventBus](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus) package installed, and your application should be an ASP.NET Core application. |
|||
|
|||
### Installation |
|||
|
|||
If your application is an ASP.NET Core application and you want to send and receive events, you need to install the [Volo.Abp.AspNetCore.Mvc.Dapr.EventBus](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus) package as described below: |
|||
|
|||
* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed it before. |
|||
* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.AspNetCore.Mvc.Dapr.EventBus` package to. |
|||
* Run the `abp add-package Volo.Abp.AspNetCore.Mvc.Dapr.EventBus` command. |
|||
|
|||
If you want to do it manually, install the [Volo.Abp.AspNetCore.Mvc.Dapr.EventBus](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus) NuGet package to your project and add `[DependsOn(typeof(AbpAspNetCoreMvcDaprEventBusModule))]` to the [ABP module](../Module-Development-Basics.md) class inside your project. |
|||
|
|||
> **If you install the [Volo.Abp.AspNetCore.Mvc.Dapr.EventBus](https://www.nuget.org/packages/Volo.Abp.AspNetCore.Mvc.Dapr.EventBus) package, you don't need to install the [Volo.Abp.EventBus.Dapr](https://www.nuget.org/packages/Volo.Abp.EventBus.Dapr) package, because the first one already has a reference to the latter one.** |
|||
|
|||
If your application is not an ASP.NET Core application, you can't receive events from Dapr, at least with ABP's integration packages (see [Dapr's document](https://docs.dapr.io/developing-applications/building-blocks/pubsub/howto-publish-subscribe/) if you want to receive events in a different type of application). However, you can still publish messages using the [Volo.Abp.EventBus.Dapr](https://www.nuget.org/packages/Volo.Abp.EventBus.Dapr) package. In this case, follow the steps below to install that package to your project: |
|||
|
|||
* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed it before. |
|||
* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.EventBus.Dapr` package to. |
|||
* Run the `abp add-package Volo.Abp.EventBus.Dapr` command. |
|||
|
|||
If you want to do it manually, install the [Volo.Abp.EventBus.Dapr](https://www.nuget.org/packages/Volo.Abp.EventBus.Dapr) NuGet package to your project and add `[DependsOn(typeof(AbpEventBusDaprModule))]` to the [ABP module](../Module-Development-Basics.md) class inside your project. |
|||
|
|||
### Configuration |
|||
|
|||
You can configure the `AbpDaprEventBusOptions` [options class](../Options.md) for Dapr configuration: |
|||
|
|||
````csharp |
|||
Configure<AbpDaprEventBusOptions>(options => |
|||
{ |
|||
options.PubSubName = "pubsub"; |
|||
}); |
|||
```` |
|||
|
|||
Available properties of the `AbpDaprEventBusOptions` class: |
|||
|
|||
* `PubSubName` (optional): The `pubsubName` parameter while publishing messages through the `DaprClient.PublishEventAsync` method. Default value: `pubsub`. |
|||
|
|||
### The ABP Subscription Endpoints |
|||
|
|||
ABP provides the following endpoints to receive events from Dapr: |
|||
|
|||
* `dapr/subscribe`: Dapr uses this endpoint to get a list of subscriptions from the application. ABP automatically returns all the subscriptions for your distributed event handler classes and custom controller actions with the `Topic` attribute. |
|||
* `api/abp/dapr/event`: The unified endpoint to receive all the events from Dapr. ABP dispatches the events to your event handlers based on the topic name. |
|||
|
|||
> **Since ABP provides the standard `dapr/subscribe` endpoint, you should not manually call the `app.MapSubscribeHandler()` method of Dapr.** You can use the `app.UseCloudEvents()` middleware in your ASP.NET Core pipeline if you want to support the [CloudEvents](https://cloudevents.io/) standard. |
|||
|
|||
### Usage |
|||
|
|||
#### The ABP Way |
|||
|
|||
You can follow [ABP's distributed event bus documentation](../Distributed-Event-Bus.md) to learn how to publish and subscribe to events in the ABP way. No change required in your application code to use Dapr pub-sub. ABP will automatically subscribe to Dapr for your event handler classes (that implement the `IDistributedEventHandler` interface). |
|||
|
|||
ABP provides `api/abp/dapr/event` |
|||
|
|||
**Example: Publish an event using the `IDistributedEventBus` service** |
|||
|
|||
````csharp |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly IDistributedEventBus _distributedEventBus; |
|||
|
|||
public MyService(IDistributedEventBus distributedEventBus) |
|||
{ |
|||
_distributedEventBus = distributedEventBus; |
|||
} |
|||
|
|||
public async Task DoItAsync() |
|||
{ |
|||
await _distributedEventBus.PublishAsync(new StockCountChangedEto |
|||
{ |
|||
ProductCode = "AT837234", |
|||
NewStockCount = 42 |
|||
}); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
**Example: Subscribe to an event by implementing the `IDistributedEventHandler` interface** |
|||
|
|||
````csharp |
|||
public class MyHandler : |
|||
IDistributedEventHandler<StockCountChangedEto>, |
|||
ITransientDependency |
|||
{ |
|||
public async Task HandleEventAsync(StockCountChangedEto eventData) |
|||
{ |
|||
var productCode = eventData.ProductCode; |
|||
// ... |
|||
} |
|||
} |
|||
```` |
|||
|
|||
See [ABP's distributed event bus documentation](../Distributed-Event-Bus.md) to learn the details. |
|||
|
|||
#### Using the Dapr API |
|||
|
|||
In addition to ABP's standard distributed event bus system, you can also use Dapr's API to publish events. |
|||
|
|||
> If you directly use the Dapr API to publish events, you may not benefit from ABP's standard distributed event bus features, like the outbox/inbox pattern implementation. |
|||
|
|||
**Example: Publish an event using `DaprClient`** |
|||
|
|||
````csharp |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly DaprClient _daprClient; |
|||
|
|||
public MyService(DaprClient daprClient) |
|||
{ |
|||
_daprClient = daprClient; |
|||
} |
|||
|
|||
public async Task DoItAsync() |
|||
{ |
|||
await _daprClient.PublishEventAsync( |
|||
"pubsub", // pubsub name |
|||
"StockChanged", // topic name |
|||
new StockCountChangedEto // event data |
|||
{ |
|||
ProductCode = "AT837234", |
|||
NewStockCount = 42 |
|||
} |
|||
); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
**Example: Subscribe to an event by creating an ASP.NET Core controller** |
|||
|
|||
````csharp |
|||
public class MyController : AbpController |
|||
{ |
|||
[HttpPost("/stock-changed")] |
|||
[Topic("pubsub", "StockChanged")] |
|||
public async Task<IActionResult> TestRouteAsync( |
|||
[FromBody] StockCountChangedEto model) |
|||
{ |
|||
HttpContext.ValidateDaprAppApiToken(); |
|||
|
|||
// Do something with the event |
|||
return Ok(); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`HttpContext.ValidateDaprAppApiToken()` extension method is provided by ABP to check if the request is coming from Dapr. This is optional. You should configure Dapr to send the App API token to your application if you want to enable the validation. If not configured, `ValidateDaprAppApiToken()` does nothing. See [Dapr's App API Token document](https://docs.dapr.io/operations/security/app-api-token/) for more information. Also see the *AbpDaprOptions* and *Security* sections in this document. |
|||
|
|||
See the [Dapr documentation](https://docs.microsoft.com/en-us/dotnet/architecture/dapr-for-net-developers/publish-subscribe) to learn the details of sending & receiving events with the Dapr API. |
|||
|
|||
## Distributed Lock |
|||
|
|||
> Dapr's distributed lock feature is currently in the Alpha stage and may not be stable yet. It is not suggested to replace ABP's distributed lock with Dapr in that point. |
|||
|
|||
ABP provides a [Distributed Locking](../Distributed-Locking.md) abstraction to control access to a shared resource by multiple applications. Dapr also has a [distributed lock building block](https://docs.dapr.io/developing-applications/building-blocks/distributed-lock/). The [Volo.Abp.DistributedLocking.Dapr](https://www.nuget.org/packages/Volo.Abp.DistributedLocking.Dapr) package makes ABP use Dapr's distributed locking system. |
|||
|
|||
### Installation |
|||
|
|||
Use the ABP CLI to add the [Volo.Abp.DistributedLocking.Dapr](https://www.nuget.org/packages/Volo.Abp.DistributedLocking.Dapr) NuGet package to your project (to the client side): |
|||
|
|||
* Install the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) if you haven't installed it before. |
|||
* Open a command line (terminal) in the directory of the `.csproj` file you want to add the `Volo.Abp.DistributedLocking.Dapr` package to. |
|||
* Run the `abp add-package Volo.Abp.DistributedLocking.Dapr` command. |
|||
|
|||
If you want to do it manually, install the [Volo.Abp.DistributedLocking.Dapr](https://www.nuget.org/packages/Volo.Abp.DistributedLocking.Dapr) NuGet package to your project and add `[DependsOn(typeof(AbpDistributedLockingDaprModule))]` to the [ABP module](../Module-Development-Basics.md) class inside your project. |
|||
|
|||
### Configuration |
|||
|
|||
You can use the `AbpDistributedLockDaprOptions` options class in the `ConfigureServices` method of [your module](../Module-Development-Basics.md) to configure the Dapr distributed lock: |
|||
|
|||
````csharp |
|||
Configure<AbpDistributedLockDaprOptions>(options => |
|||
{ |
|||
options.StoreName = "mystore"; |
|||
}); |
|||
```` |
|||
|
|||
The following options are available: |
|||
|
|||
* **`StoreName`** (required): The store name used by Dapr. Lock key names are scoped in the same store. That means different applications can acquire the same lock name in different stores. Use the same store name for the same resources you want to control the access of. |
|||
* `Owner` (optional): The `owner` value used by the `DaprClient.Lock` method. If you don't specify, ABP uses a random value, which is fine in general. |
|||
* `DefaultExpirationTimeout` (optional): Default value of the time after which the lock gets expired. Default value: 2 minutes. |
|||
|
|||
### Usage |
|||
|
|||
You can inject and use the `IAbpDistributedLock` service, just like explained in the [Distributed Locking document](../Distributed-Locking.md). |
|||
|
|||
**Example:** |
|||
|
|||
````csharp |
|||
public class MyService : ITransientDependency |
|||
{ |
|||
private readonly IAbpDistributedLock _distributedLock; |
|||
|
|||
public MyService(IAbpDistributedLock distributedLock) |
|||
{ |
|||
_distributedLock = distributedLock; |
|||
} |
|||
|
|||
public async Task MyMethodAsync() |
|||
{ |
|||
await using (var handle = |
|||
await _distributedLock.TryAcquireAsync("MyLockName")) |
|||
{ |
|||
if (handle != null) |
|||
{ |
|||
// your code that access the shared resource |
|||
} |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
There are two points we should mention about the `TryAcquireAsync` method, as different from ABP's standard usage: |
|||
|
|||
* The `timeout` parameter is currently not used (even if you specify it), because Dapr doesn't support waiting to obtain the lock. |
|||
* Dapr uses the expiration timeout system (that means the lock is automatically released after that timeout even if you don't release the lock by disposing the handler). However, ABP's `TryAcquireAsync` method has no such a parameter. Currently, you can set `AbpDistributedLockDaprOptions.DefaultExpirationTimeout` as a global value in your application. |
|||
|
|||
As mentioned first, Dapr's distributed lock feature is currently in the Alpha stage and its API is a candidate to change. You should use it as is if you want, but be ready for the changes in the future. For now, we are recommending to use the [DistributedLock](https://github.com/madelson/DistributedLock) library as explained in ABP's [Distributed Locking document](../Distributed-Locking.md). |
|||
|
|||
## Security |
|||
|
|||
If you are using Dapr, most or all the incoming and outgoing requests in your application pass through Dapr. Dapr uses two kinds of API tokens to secure the communication between your application and Dapr. |
|||
|
|||
### Dapr API Token |
|||
|
|||
> This token is automatically set by default and generally you don't care about it. |
|||
|
|||
The [Enable API token authentication in Dapr](https://docs.dapr.io/operations/security/api-token/) document describes what the Dapr API token is and how it is configured. Please read that document if you want to enable it for your application. |
|||
|
|||
If you enable the Dapr API token, you should send that token in every request to Dapr from your application. `AbpDaprOptions` defines a `DaprApiToken` property as a central point to configure the Dapr API token in your application. |
|||
|
|||
The default value of the `DaprApiToken` property is set from the `DAPR_API_TOKEN` environment variable and that environment variable is set by Dapr when it runs. So, most of the time, you don't need to configure `AbpDaprOptions.DaprApiToken` in your application. However, if you need to configure (or override) it, you can do in the `ConfigureServices` method of your module class as shown in the following code block: |
|||
|
|||
````csharp |
|||
Configure<AbpDaprOptions>(options => |
|||
{ |
|||
options.DaprApiToken = "..."; |
|||
}); |
|||
```` |
|||
|
|||
Or you can set it in your `appsettings.json` file: |
|||
|
|||
````json |
|||
"Dapr": { |
|||
"DaprApiToken": "..." |
|||
} |
|||
```` |
|||
|
|||
Once you set it, it is used when you inject `DaprClient` or use `IAbpDaprClientFactory`. If you need that value in your application, you can inject `IDaprApiTokenProvider` and use its `GetDaprApiToken()` method. |
|||
|
|||
### App API Token |
|||
|
|||
> Enabling App API token validation is strongly recommended. Otherwise, for example, any client can directly call your event subscription endpoint, and your application acts like an event has occurred (if there is no other security policy in your event subscription endpoint). |
|||
|
|||
The [Authenticate requests from Dapr using token authentication](https://docs.dapr.io/operations/security/app-api-token/) document describes what the App API token is and how it is configured. Please read that document if you want to enable it for your application. |
|||
|
|||
If you enable the App API token, you can validate it to ensure that the request is coming from Dapr. ABP provides useful shortcuts to validate it. |
|||
|
|||
**Example: Validate the App API token in an event handling HTTP API** |
|||
|
|||
````csharp |
|||
public class MyController : AbpController |
|||
{ |
|||
[HttpPost("/stock-changed")] |
|||
[Topic("pubsub", "StockChanged")] |
|||
public async Task<IActionResult> TestRouteAsync( |
|||
[FromBody] StockCountChangedEto model) |
|||
{ |
|||
// Validate the App API token! |
|||
HttpContext.ValidateDaprAppApiToken(); |
|||
|
|||
// Do something with the event |
|||
return Ok(); |
|||
} |
|||
} |
|||
```` |
|||
|
|||
`HttpContext.ValidateDaprAppApiToken()` is an extension method provided by the ABP Framework. It throws an `AbpAuthorizationException` if the token was missing or wrong in the HTTP header (the header name is `dapr-api-token`). You can also inject `IDaprAppApiTokenValidator` and use its methods to validate the token in any service (not only in a controller class). |
|||
|
|||
You can configure `AbpDaprOptions.AppApiToken` if you want to set (or override) the App API token value. The default value is set by the `APP_API_TOKEN` environment variable. You can change it in the `ConfigureServices` method of your module class as shown in the following code block: |
|||
|
|||
````csharp |
|||
Configure<AbpDaprOptions>(options => |
|||
{ |
|||
options.AppApiToken = "..."; |
|||
}); |
|||
```` |
|||
|
|||
Or you can set it in your `appsettings.json` file: |
|||
|
|||
````json |
|||
"Dapr": { |
|||
"AppApiToken": "..." |
|||
} |
|||
```` |
|||
|
|||
If you need that value in your application, you can inject `IDaprApiTokenProvider` and use its `GetAppApiToken()` method. |
|||
|
|||
## See Also |
|||
|
|||
* [Dapr for .NET Developers](https://docs.microsoft.com/en-us/dotnet/architecture/dapr-for-net-developers/) |
|||
* [The Official Dapr Documentation](https://docs.dapr.io/) |
|||
@ -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 |
|||
|
|||
 |
|||
|
|||
 |
|||