Merge branch 'dev' into IAbpHostEnvironment

pull/14842/head
maliming 3 years ago
commit 836f687cc2

@ -210,7 +210,7 @@
"TrialPlan": "Do you have a trial plan?",
"TrialPlanExplanation": "It has a 14 days trial period for the ABP Commercial team license. For more information visit <a href={0} target='_blank'>here</a>. Furthermore, for the Team licenses we provide a 30 days money-back guarantee. You can just request a refund in the first 30 days. For the Business and Enterprise licenses, we provide 60% refund in 30 days. This is because Business and Enterprise licenses include the full source code of all the modules and the themes.",
"DoYouAcceptBankWireTransfer": "Do you accept bank wire transfers?",
"DoYouAcceptBankWireTransferExplanation": "Yes, we accept bank wire transfers.<br/>After sending the license fee via bank transfer, send your receipt and requested license type to accounting@abp.io.<br/>Our international bank account information:",
"DoYouAcceptBankWireTransferExplanation": "Yes, we accept bank wire transfers.<br/>After sending the license fee via bank transfer, send your receipt and requested license type to accounting@volosoft.com.<br/>Our international bank account information:",
"HowToUpgrade": "How to upgrade existing applications when a new version is available?",
"HowToUpgradeExplanation1": "When you create a new application using ABP Commercial, all the modules and theme are used as NuGet and NPM packages. So, you can easily upgrade the packages when a new version is available.",
"HowToUpgradeExplanation2": "In addition to the standard NuGet/NPM upgrades, <a href=\"{0}\">ABP CLI</a> provides an update command that automatically finds and upgrades all ABP related packages in your solution.",

@ -2,13 +2,14 @@
ABP Framework provides a pre-built and standard endpoint that contains some useful information about the application/service. Here, the list of some fundamental information at this endpoint:
* [Localization](../Localization.md) values, supported and the current language of the application.
* Available and granted [policies](../Authorization.md) (permissions) for the current user.
* Granted [policies](../Authorization.md) (permissions) for the current user.
* [Setting](../Settings.md) values for the current user.
* Info about the [current user](../CurrentUser.md) (like id and user name).
* Info about the current [tenant](../Multi-Tenancy.md) (like id and name).
* [Time zone](../Timing.md) information for the current user and the [clock](../Timing.md) type of the application.
> If you have started with ABP's startup solution templates and using one of the official UI options, then all these are set up for you and you don't need to know these details. However, if you are building a UI application from scratch, you may want to know this endpoint.
## HTTP API
If you navigate to the `/api/abp/application-configuration` URL of an ABP Framework based web application or HTTP Service, you can access the configuration as a JSON object. This endpoint is useful to create the client of your application.
@ -19,5 +20,5 @@ For ASP.NET Core MVC (Razor Pages) applications, the same configuration values a
See the [JavaScript API document](../UI/AspNetCore/JavaScript-API/Index.md) for the ASP.NET Core UI.
Other UI types provide services native to the related platform. For example, see the [Angular UI localization documentation](../UI/Angular/Localization.md) to learn how to use the localization values exposes by this endpoint.
Other UI types provide services native to the related platform. For example, see the [Angular UI settings documentation](../UI/Angular/Settings.md) to learn how to use the setting values exposes by this endpoint.

@ -0,0 +1,33 @@
# Application Localization Endpoint
ABP Framework provides a pre-built and standard endpoint that returns all the [localization](../Localization.md) resources and texts defined in the server.
> If you have started with ABP's startup solution templates and using one of the official UI options, then all these are set up for you and you don't need to know these details. However, if you are building a UI application from scratch, you may want to know this endpoint.
## HTTP API
`/api/abp/application-localization` is the main URL of the HTTP API that returns the localization data as a JSON string. I accepts the following query string parameters:
* `cultureName` (required): A culture code to get the localization data, like `en` or `en-US`.
* `onlyDynamics` (optional, default: `false`): Can be set to `true` to only get the dynamically defined localization resources and texts. If your client-side application shares the same localization resources with the server (like ABP's Blazor and MVC UIs), you can set `onlyDynamics` to `true`.
**Example request:**
````
/api/abp/application-localization?cultureName=en
````
## Script
For [ASP.NET Core MVC (Razor Pages)](../UI/AspNetCore/Overall.md) applications, the same localization data is also available on the JavaScript side. `/Abp/ApplicationLocalizationScript` is the URL of the script that is auto-generated based on the HTTP API above.
**Example request:**
````
/Abp/ApplicationLocalizationScript?cultureName=en
````
See the [JavaScript API document](../UI/AspNetCore/JavaScript-API/Index.md) for the ASP.NET Core UI.
Other UI types provide services native to the related platform. For example, see the [Angular UI localization documentation](../UI/Angular/Localization.md) to learn how to use the localization values exposes by this endpoint.

@ -53,7 +53,7 @@ public class MyLogWorker : HangfireBackgroundWorkerBase
CronExpression = Cron.Daily();
}
public override Task DoWorkAsync()
public override Task DoWorkAsync(CancellationToken cancellationToken = default)
{
Logger.LogInformation("Executed MyLogWorker..!");
return Task.CompletedTask;
@ -68,14 +68,7 @@ public class MyLogWorker : HangfireBackgroundWorkerBase
### UnitOfWork
For use with `UnitOfWorkAttribute`, you need to define an interface for worker:
```csharp
public interface IMyLogWorker : IHangfireBackgroundWorker
{
}
[ExposeServices(typeof(IMyLogWorker))]
public class MyLogWorker : HangfireBackgroundWorkerBase, IMyLogWorker
{
public MyLogWorker()
@ -84,11 +77,13 @@ public class MyLogWorker : HangfireBackgroundWorkerBase, IMyLogWorker
CronExpression = Cron.Daily();
}
[UnitOfWork]
public override Task DoWorkAsync()
public override Task DoWorkAsync(CancellationToken cancellationToken = default)
{
Logger.LogInformation("Executed MyLogWorker..!");
return Task.CompletedTask;
using (var uow = LazyServiceProvider.LazyGetRequiredService<IUnitOfWorkManager>().Begin())
{
Logger.LogInformation("Executed MyLogWorker..!");
return Task.CompletedTask;
}
}
}
```
@ -105,9 +100,6 @@ public class MyModule : AbpModule
ApplicationInitializationContext context)
{
await context.AddBackgroundWorkerAsync<MyLogWorker>();
//If the interface is defined
//await context.AddBackgroundWorkerAsync<IMyLogWorker>();
}
}
````

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

@ -0,0 +1,134 @@
# JSON Columns in Entity Framework Core 7
In this article, we will see how to use the new **JSON Columns** features that came with EF Core 7 in an ABP based application (with examples).
## JSON Columns
Most relational databases support columns that contain JSON documents. The JSON in these columns can be drilled into with queries. This allows, for example, filtering and sorting by the elements of the documents, as well as projection of elements out of the documents into results. JSON columns allow relational databases to take on some of the characteristics of document databases, creating a useful hybrid between these two database management approaches.
EF7 contains provider-agnostic support for JSON columns, with an implementation for SQL Server. This support allows the mapping of aggregates built from .NET types to JSON documents. Normal LINQ queries can be used on the aggregates, and these will be translated to the appropriate query constructs needed to drill into the JSON. EF7 also supports updating and saving changes to JSON documents.
> You can find more information about JSON columns in EF Core's [documentation](https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#json-columns).
### Mapping JSON Columns
In EF Core, aggregate types can be defined using `OwnsOne` and `OwnsMany` methods. `OwnsOne` can be used to map a single aggregate and the `OwnsMany` method can be used to map a collection of aggregates.
With EF 7, we have a new extension method for mapping property to a JSON Column: `ToJson`. We can use this method to mark a property as a JSON Column. The property can be of any type that can be serialized to JSON.
The following example shows how to map a JSON column to an aggregate type:
```csharp
public class ContactDetails
{
public Address Address { get; set; }
public string? Phone { get; set; }
}
public class Address
{
public Address(string street, string city, string postcode, string country)
{
Street = street;
City = city;
Postcode = postcode;
Country = country;
}
public string Street { get; set; }
public string City { get; set; }
public string Postcode { get; set; }
public string Country { get; set; }
}
public class Person : AggregateRoot<int>
{
public string Name { get; set; } = null!;
public ContactDetails ContactDetails { get; set; } = null!;
}
```
* Above, we have defined an aggregate type `ContactDetails` that contains an `Address` and a `Phone` number. The aggregate type is configured in `OnModelCreating` using `OwnsOne` and `ToJson` methods below.
* The `Address` property is mapped to a JSON column using `ToJson`, and the `Phone` property is mapped to a regular column. This requires just one call to **ToJson()** when configuring the aggregate type:
```csharp
public class MyDbContext : AbpDbContext<MyDbContext>
{
public DbSet<Person> Persons { get; set; }
public MyDbContext(DbContextOptions<MyDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<Person>(b =>
{
b.ToTable(MyProjectConsts.DbTablePrefix + "Persons", MyProjectConsts.DbSchema);
b.ConfigureByConvention();
b.OwnsOne(x=>x.ContactDetails, c =>
{
c.ToJson(); //mark as JSON Column
c.OwnsOne(cd => cd.Address);
});
});
}
}
```
### Querying JSON Columns
Queries into JSON columns work just the same as querying into any other aggregate type in EF Core. That's it, just use the LINQ! Here are some examples:
```csharp
var persons = await (await GetDbSetAsync()).ToListAsync();
var contacts = await (await GetDbSetAsync()).Select(person => new
{
person,
person.ContactDetails.Phone, //query over JSON column
Addresses = person.ContactDetails.Address //query over JSON column
}).ToListAsync();
var addresses = await (await GetDbSetAsync()).Select(person => new
{
person,
Addresses = person.ContactDetails.Address //query over JSON column
}).ToListAsync();
```
### Updating JSON Columns
You can update JSON columns the same as updating any record by using the `UpdateAsync` method. The following example shows how to update a JSON column:
```csharp
var person = await (await GetDbSetAsync()).FirstAsync();
person.ContactDetails.Phone = "123456789";
person.ContactDetails.Address = new Address("Street", "City", "Postcode", "Country");
await UpdateAsync(person, true);
```
### JSON Column in a Database
After you've configured the database relations, created a new migration and applied it to database you will have a database table like below:
![image](./Database.png)
As you can see, thanks to JSON Columns feature the **ContactDetails** row has JSON content and we can use it in a query or update it from our application with the LINQ JSON query support that mentioned above.
### Conclusion
In this article, I've briefly introduced the JSON Columns feature that was shipped with EF Core 7. It's pretty straightforward to use JSON Columns in an ABP based application. You can see the examples above and give it a try!
### The Source Code
* You can find the full source code of the example application [here](https://github.com/abpframework/abp-samples/tree/master/EfCoreJSONColumnDemo).
### References
* [https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#json-columns](https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#json-columns)
* [https://docs.microsoft.com/en-us/ef/core/modeling/owned-entities](https://docs.microsoft.com/en-us/ef/core/modeling/owned-entities)

@ -0,0 +1,73 @@
# gRPC - Health Checks
In this article we will show how to use gRPC health checks with the ABP Framework.
## Health Checks
ASP.NET Core 7 supports gRPC health checks. Health Checks allow us to determine the overall health and availability of our application infrastructure. They are exposed as HTTP endpoints and can be configured to provide information for various monitoring scenarios, such as the response time and memory usage of our application, or whether our application can communicate with our database provider.
### gRPC Health Checks
The [gRPC health checking protocol](https://github.com/grpc/grpc/blob/master/doc/health-checking.md) is a standard for reporting the health of gRPC server apps. An app exposes health checks as a gRPC service. They are typically used with an external monitoring service to check the status of an app.
### Grpc.AspNetCore.HealthChecks
ASP.NET Core supports the gRPC health checking protocol with the [Grpc.AspNetCore.HealthChecks](https://www.nuget.org/packages/Grpc.AspNetCore.HealthChecks) package. Results from .NET health checks are reported to callers.
## Using gRPC Health Checks with the ABP Framework
In this article, I'm assuming you've used gRPC with ABP before. If you are still having problems with this, it may be good for you to review this article.
https://community.abp.io/posts/using-grpc-with-the-abp-framework-2dgaxzw3
### Set up gRPC Health Checks
In this solution, `*.HttpApi.Host` is the project that configures and runs the server-side application. So, we will make changes in that project.
* Add the `Grpc.AspNetCore.HealthChecks` package to your project.
```bash
dotnet add package Grpc.AspNetCore.HealthChecks
```
* `AddGrpcHealthChecks` to register services that enable health checks.
```csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
// Other configurations...
context.Services.AddGrpcHealthChecks()
.AddCheck("SampleHealthCheck", () => HealthCheckResult.Healthy());
}
```
* `MapGrpcHealthChecksService` to add a health check service endpoint.
```csharp
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
// Other middlewares...
app.UseConfiguredEndpoints(builder =>
{
builder.MapGrpcHealthChecksService();
});
}
```
### Calling Health Checks From a Client
Now that our server is configured for gRPC health checks, we can test it by creating a basic console client.
```csharp
var channel = GrpcChannel.ForAddress("https://localhost:44357");
var client = new Health.HealthClient(channel);
var response = await client.CheckAsync(new HealthCheckRequest());
var status = response.Status;
Console.WriteLine($"Health Status: {status}");
```
## References
- https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-7.0?view=aspnetcore-7.0#grpc-health-checks-in-aspnet-core

@ -0,0 +1,381 @@
# Injecting Service Dependencies to Entities with Entity Framework Core 7.0
[Dependency injection](https://docs.abp.io/en/abp/latest/Dependency-Injection) is a widely-used pattern of obtaining references to other services from our classes. It is a built-in feature when you develop ASP.NET Core applications. In this article, I will explain why we may need to have references to other services in an entity class and how we can implement Entity Framework Core's new `IMaterializationInterceptor` interface to provide these services to the entities using the standard dependency injection system.
> You can find the source code of the example application [here](https://github.com/abpframework/abp-samples/tree/master/EfCoreEntityDependencyInjectionDemo).
## The Problem
While developing applications based on [Domain-Driven Design](https://docs.abp.io/en/abp/latest/Domain-Driven-Design) (DDD) patterns, we typically write our business code inside [application services](https://docs.abp.io/en/abp/latest/Application-Services), [domain services](https://docs.abp.io/en/abp/latest/Domain-Services) and [entities](https://docs.abp.io/en/abp/latest/Entities). Since the application and domain service instances are created by the dependency injection system, they can inject services into their constructors.
Here, an example domain service that injects a repository into its constructor:
````csharp
public class ProductManager : DomainService
{
private readonly IRepository<Product, Guid> _productRepository;
public ProductManager(IRepository<Product, Guid> productRepository)
{
_productRepository = productRepository;
}
//...
}
````
`ProductManager` can then use the `_productRepository` object in its methods to perform its business logic. In the following example, `ChangeCodeAsync` method is used to change a product's code (the `ProductCode` property) by ensuring uniqueness of product codes in the system:
````csharp
public class ProductManager : DomainService
{
private readonly IRepository<Product, Guid> _productRepository;
public ProductManager(IRepository<Product, Guid> productRepository)
{
_productRepository = productRepository;
}
public async Task ChangeCodeAsync(Product product, string newProductCode)
{
Check.NotNull(product, nameof(product));
Check.NotNullOrWhiteSpace(newProductCode, nameof(newProductCode));
if (product.ProductCode == newProductCode)
{
return;
}
if (await _productRepository.AnyAsync(x => x.ProductCode == newProductCode))
{
throw new ApplicationException(
"Product code is already used: " + newProductCode);
}
product.ProductCode = newProductCode;
}
}
````
Here, the `ProductManager` forces the rule "product code must be unique". Let's see the `Product` entity class too:
````csharp
public class Product : AuditedAggregateRoot<Guid>
{
public string ProductCode { get; internal set; }
public string Name { get; private set; }
private Product()
{
/* This constructor is used by EF Core while
getting the Product from database */
}
/* Primary constructor that should be used in the application code */
public Product(string productCode, string name)
{
ProductCode = Check.NotNullOrWhiteSpace(productCode, nameof(productCode));
Name = Check.NotNullOrWhiteSpace(name, nameof(name));
}
}
````
You see that the `ProductCode` property's setter is `internal`, which makes possible to set it from the `ProductManager` class as shown before.
This design has a problem: We had to make the `ProductCode` setter `internal`. Now, any developer may forget to use the `ProductManager.ChangeCodeAsync` method, and can directly set the `ProductCode` on the entity. So, we can't completely force the "product code must be unique" rule.
It would be better to move the `ChangeCodeAsync` method into the `Product` class and make the `ProductCode` property's setter `private`:
````csharp
public class Product : AuditedAggregateRoot<Guid>
{
public string ProductCode { get; private set; }
public string Name { get; private set; }
// ...
public async Task ChangeCodeAsync(string newProductCode)
{
Check.NotNullOrWhiteSpace(newProductCode, nameof(newProductCode));
if (newProductCode == ProductCode)
{
return;
}
/* ??? HOW TO INJECT THE PRODUCT REPOSITORY HERE ??? */
if (await _productRepository.AnyAsync(x => x.ProductCode == newProductCode))
{
throw new ApplicationException("Product code is already used: " + newProductCode);
}
ProductCode = newProductCode;
}
}
````
With that design, there is no way to set the `ProductCode` without applying the rule "product code must be unique". Great! But we have a problem: An entity class can not inject dependencies into its constructor, because an entity is not created using the dependency injection system. There are two common points of creating an entity:
* We can create an entity in our application code, using the standard `new` keyword, like `var product = new Product(...);`.
* Entity Framework (and any other ORM / database provider) creates entities after getting them from the database. They typically use the empty (default) constructor of the entity to create it, then sets the properties coming from the database query.
So, how we can use the product repository in the `Product.ChangeCodeAsync` method? If we forget the dependency injection system, we would think to add the repository as a parameter to the `ChangeCodeAsync` method and delegate the responsibility of obtaining the service reference to the caller of that method:
````csharp
public async Task ChangeCodeAsync(
IRepository<Product, Guid> productRepository, string newProductCode)
{
Check.NotNull(productRepository, nameof(productRepository));
Check.NotNullOrWhiteSpace(newProductCode, nameof(newProductCode));
if (newProductCode == ProductCode)
{
return;
}
if (await productRepository.AnyAsync(x => x.ProductCode == newProductCode))
{
throw new ApplicationException(
"Product code is already used: " + newProductCode);
}
ProductCode = newProductCode;
}
````
However, that design would make hard to use the `ChangeCodeAsync` method, and also exposes its internal dependencies to outside. If we need another dependency in the `ChangeCodeAsync` method later, we should add another parameter, which will effect all the application code that uses the `ChangeCodeAsync` method. I think that's not reasonable. The next section offers a better and a more generic solution to the problem.
## The Solution
First of all, we can introduce an interface that should be implemented by the entity classes which needs to use services in their methods:
````csharp
public interface IInjectServiceProvider
{
ICachedServiceProvider ServiceProvider { get; set; }
}
````
`ICachedServiceProvider` is a service that is provided by the ABP Framework. It extends the standard `IServiceProvider`, but caches the resolved services. Basically, it internally resolves a service only a single time, even if you resolve the service from it multiple times. The `ICachedServiceProvider` service itself is a scoped service, means it is created only once in a scope. We can use it to optimize the service resolution, however, the standard `IServiceProvider` would work as expected.
Next, we can implement the `IInjectServiceProvider` for our `Product` entity:
````csharp
public class Product : AuditedAggregateRoot<Guid>, IInjectServiceProvider
{
public ICachedServiceProvider ServiceProvider { get; set; }
//...
}
````
I will explain how to set the `ServiceProvider` property later, but first see how to use it in our `Product.ChangeCodeAsync` method. Here, the final `Product` class:
````csharp
public class Product : AuditedAggregateRoot<Guid>, IInjectServiceProvider
{
public string ProductCode { get; internal set; }
public string Name { get; private set; }
public ICachedServiceProvider ServiceProvider { get; set; }
private Product()
{
/* This constructor is used by EF Core while
getting the Product from database */
}
/* Primary constructor that should be used in the application code */
public Product(string productCode, string name)
{
ProductCode = Check.NotNullOrWhiteSpace(productCode, nameof(productCode));
Name = Check.NotNullOrWhiteSpace(name, nameof(name));
}
public async Task ChangeCodeAsync(string newProductCode)
{
Check.NotNullOrWhiteSpace(newProductCode, nameof(newProductCode));
if (newProductCode == ProductCode)
{
return;
}
var productRepository = ServiceProvider
.GetRequiredService<IRepository<Product, Guid>>();
if (await productRepository.AnyAsync(x => x.ProductCode == newProductCode))
{
throw new ApplicationException("Product code is already used: " + newProductCode);
}
ProductCode = newProductCode;
}
}
````
The `ChangeCodeAsync` method gets the product repository from the `ServiceProvider` and uses it to check if there is another product with the given `newProductCode` value.
Now, let's explain how to set the `ServiceProvider` value...
### Entity Framework Core Configuration
Entity Framework 7.0 introduces the `IMaterializationInterceptor` interceptor that allows us to manipulate an entity object just after the entity object is created as a result of database query.
We can write the following interceptor that sets the `ServiceProvider` property of an entity, if it implements the `IInjectServiceProvider` interface:
````csharp
public class ServiceProviderInterceptor : IMaterializationInterceptor
{
public object InitializedInstance(
MaterializationInterceptionData materializationData,
object instance)
{
if (instance is IInjectServiceProvider entity)
{
entity.ServiceProvider = materializationData
.Context
.GetService<ICachedServiceProvider>();
}
return instance;
}
}
````
> Lifetime of the resolved services are tied to the lifetime of the related `DbContext` instance. So, you don't need to care if the resolved dependencies are disposed. ABP's [unit of work](https://docs.abp.io/en/abp/latest/Unit-Of-Work) system already disposes the `DbContext` instance when the unit of work is completed.
Once we defined such an interceptor, we should configure our `DbContext` class to use it. You can do it by overriding the `OnConfiguring` method in your `DbContext` class:
````csharp
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.AddInterceptors(new ServiceProviderInterceptor());
}
````
Finally, you should ignore the `ServiceProvider` property in your entity mapping configuration in your `DbContext` (because we don't want to map it to a database table field):
````csharp
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// ...
builder.Entity<Product>(b =>
{
// ...
/* We should ignore the ServiceProvider on mapping! */
b.Ignore(x => x.ServiceProvider);
});
}
````
That's all. From now, EF Core will set the `ServiceProvider` property for you.
### Manually Creating Entities
While EF Core seamlessly set the `ServiceProvider` property while getting entities from database, you should still set it manually while creating new entities yourself.
**Example: Set `ServiceProvider` property while creating a new Product entity:**
````csharp
public async Task CreateAsync(CreateProductDto input)
{
var product = new Product(input.ProductCode, input.Name)
{
ServiceProvider = _cachedServiceProvider
};
await _productRepository.InsertAsync(product);
}
````
Here, you may think that it is not necessary to set the `ServiceProvider`, because we haven't used the `ChangeCodeAsync` method. You are definitely right; It is not needed in this example, because it is clear to see the entity object is not used between the entity creation and saving it to the database. However, if you call a method of the entity, or pass it to another service before inserting into the database, you may not know if the `ServiceProvider` will be needed. So, you should carefully use it.
Basically, I've introduced the problem and the solution. In the next section, I will explain some limitations of that design and some of my other thoughts.
## Discussions
In this section, I will first discuss a slightly different way of obtaining services. Then I will explain limitations and problems of injecting services into entities.
### Why injected a service provider, but not the services?
As an obvious question, you may ask why we've property-injected a service provider object, then resolved the services manually. Can't we directly property-inject our dependencies?
**Example: Property-inject the `IRepository<Product, Guid>` service:**
````csharp
public class Product : AuditedAggregateRoot<Guid>
{
// ...
public IRepository<Product, Guid> ProductRepository { get; set; }
public async Task ChangeCodeAsync(string newProductCode)
{
Check.NotNullOrWhiteSpace(newProductCode, nameof(newProductCode));
if (newProductCode == ProductCode)
{
return;
}
if (await ProductRepository.AnyAsync(x => x.ProductCode == newProductCode))
{
throw new ApplicationException("Product code is already used: " + newProductCode);
}
ProductCode = newProductCode;
}
}
````
Now, we don't need to implement the `IInjectServiceProvider` interface and manually resolve the `IRepository<Product, Guid>` object from the `ServiceProvider`. You see that the `ChangeCodeAsync` method is much simpler now.
So, how to set `ProductRepository`? For the EF Core interceptor part, you can somehow get all public properties of the entity via reflection. Then, for each property, check if such a service does exist, and set it from the dependency injection system if available. Surely, that will be less performant, but will work if you can truly implement. On the other hand, it would be extra hard to set all the dependencies of the entity while manually creating it using the `new` keyword. So, personally I wouldn't recommend that approach.
### Limitations
One important limitation is that you can not use the services inside your entity's constructor code. Ideally, the constructor of the `Product` class should check if the product code is already used before. See the following constructor:
````csharp
public Product(string productCode, string name)
{
ProductCode = Check.NotNullOrWhiteSpace(productCode, nameof(productCode));
Name = Check.NotNullOrWhiteSpace(name, nameof(name));
/* Can not check if product code is already used by another product? */
}
````
It is not possible to use the product repository here, because;
1. The services are property-injected. That means they will be set after the object creation has completed.
2. Even if the service is available, it won't be truly possible to call async code in a constructor. You know constructors can not be async in C#, but the repository and other service methods are generally designed as async.
So, if you want to force the "product code must be unique" rule, you should create an async domain service method (like `ProductManager.CreateAsync(...)`) and always use it to create products (you can make the `Product` class constructor `internal` to not allow to use it in the application layer).
### Design Problems
Beside the technical limitations, coupling your entities to external services is generally considered as a bad design. It makes your entities over-complicated, hard to test, and generally leads to take too much responsibility over the time.
## Conclusion
In this article, I tried to investigate all aspects of injecting services into entity classes. I explained how to use Entity Framework 7.0 `IMaterializationInterceptor` to implement property-injection pattern while getting entities from database.
Injecting services into entities seems a certain way of forcing some business rules in your entities. However, because of the current technical limitations, design issues and usage difficulties, I don't suggest to depend on services in your entities. Instead, create domain services when you need to implement a business rule that depends on external services and entities.
## The Source Code
* You can find the full source code of the example application [here](https://github.com/abpframework/abp-samples/tree/master/EfCoreEntityDependencyInjectionDemo).
* You can see [this pull request](https://github.com/abpframework/abp-samples/pull/207/files) for the changes I've done after creating the application.
## See Also
* [What's new in EF Core 7.0](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew)
* [ABP Framework: Dependency Injection](https://docs.abp.io/en/abp/latest/Dependency-Injection)
* [ABP Framework: Domain Driven Design](https://docs.abp.io/en/abp/latest/Domain-Driven-Design)

@ -0,0 +1,88 @@
# Model building conventions in Entity Framework Core 7.0
In this article, I will show you one of the new features of EF Core 7 named "Model building conventions".
Entity Framework Core uses a metadata model to describe how entity types are mapped to the database. Before EF Core 7.0, it was not possible to remove or replace existing conventions or add new conventions. With EF Core 7.0, this is now possible. To read more about it, you can visit its [documentation](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#model-building-conventions).
EF Core uses many built-in conventions. You can see the full list of the conventions on `IConvetion` Interface API [documentation](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.metadata.conventions.iconvention?view=efcore-7.0).
If you want to add, remove or replace a convention, you need to override `ConfigureConventions` method of your DbContext as shown below;
```csharp
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Add(_ => new MyCustomConvention());
}
```
## Allowed Operations
### Removing an existing convention
Existing conventions provided by EF Core are well thought and useful, but sometimes some of them might not be a good candidate for your application. In such cases, you can remove an existing convention as shown below;
```csharp
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Remove(typeof(ForeignKeyIndexConvention));
}
```
### Adding a new convention
Just like removing a convention, we can add a completely new convention as well. You can define many different conventions here. For example, you can specify a standard precision for all decimal fields in your entities.
```csharp
public class DecimalPrecisionConvention : IModelFinalizingConvention
{
public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
{
foreach (var property in modelBuilder.Metadata.GetEntityTypes()
.SelectMany(
entityType => entityType.GetDeclaredProperties()
.Where(
property => property.ClrType == typeof(decimal))))
{
property.Builder.HasPrecision(2);
}
}
}
```
Note that, conventions are executed in the order they are added. So you need to be careful in which order they are added.
### Replacing an existing convention
Sometimes, a default convention might work slightly different than what your app expects. In such cases, you can create your own implementation by inheriting from that convention and replace the default one. For example, you can create a convention as shown below;
```csharp
public class MyCustomConvention : ADefaultEfCoreConvention
{
public MyCustomConvention(ProviderConventionSetBuilderDependencies dependencies)
: base(dependencies)
{
}
// override the methods you want to change.
}
```
Then, you can replace the default one with your implementation as shown below;
```csharp
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Conventions.Replace<ADefaultEfCoreConvention>(
serviceProvider => new MyCustomConvention(
serviceProvider.GetRequiredService<ProviderConventionSetBuilderDependencies>()));
}
```
As a final note, conventions never override configuration marked as **DataAnnotation** or **Explicit**. This means that, even if there is a convention, if the property has a `DataAnnotation` attribute or configuration in `OnModelCreating`, convetion will not be used. Here are the configuration types EF Core uses;
* **Explicit:** The model element was explicitly configured in OnModelCreating
* **DataAnnotation:** The model element was configured using a mapping attribute (aka data annotation) on the CLR type
* **Convention:** The model element was configured by a model building convention
## Using in ABP-based solution
Since ABP uses EF Core, you can use this feature in ABP as well.

@ -0,0 +1,71 @@
# Bulk Operations with Entity Framework Core 7.0
Entity Framework tracks all the entity changes and applies those changes to the database one by one when the `SaveChanges()` method is called. There was no way to execute bulk operations in Entity Framework Core without a dependency.
As you know the [Entity Framework Extensions](https://entityframework-extensions.net/bulk-savechanges) library was doing it but it was not free.
There was no other solution until now. [Bulk Operations](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#executeupdate-and-executedelete-bulk-updates) are now available in Entity Framework Core with .NET 7.
With .NET 7, there are two new methods such as `ExecuteUpdate` and `ExecuteDelete` available to execute bulk operations. It's a similar usage with the Entity Framework Core Extensions library if you're familiar with it.
You can visit the microsoft example [here](https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#executeupdate-and-executedelete-bulk-updates) about how to use it.
It can be easily used with the DbContext.
```csharp
await context.Tags.Where(t => t.Text.Contains(".NET")).ExecuteDeleteAsync();
```
## Using with ABP Framework
ABP Framework provides an abstraction over database operations and implements generic repository pattern. So, DbContext can't be accessed outside of [repositories](https://docs.abp.io/en/abp/latest/Repositories).
You can use the `ExecuteUpdate` and `ExecuteDelete` methods inside a repository.
```csharp
public class BookEntityFrameworkCoreRepository : EfCoreRepository<BookStoreDbContext, Book, Guid>, IBookRepository
{
public BookEntityFrameworkCoreRepository(IDbContextProvider<BookStoreDbContext> dbContextProvider) : base(dbContextProvider)
{
}
public async Task UpdateListingAsync()
{
var dbSet = await GetDbSetAsync();
await dbSet
.Where(x => x.IsListed && x.PublishedOn.Year <= 2022)
.ExecuteUpdateAsync(s => s.SetProperty(x => x.IsListed, x => false));
}
public async Task DeleteOldBooksAsync()
{
var dbSet = await GetDbSetAsync();
await dbSet
.Where(x => x.PublishedOn.Year <= 2000)
.ExecuteDeleteAsync();
}
}
```
There is no need to take an action for bulk inserting. You can use the `InsertManyAsync` method of the repository instead of creating a new method for it if you don't have custom logic. It'll use a new bulk inserting feature automatically since it's available in EF Core 7.0.
```csharp
public class MyDomainService : DomainService
{
protected IRepository<Book, Guid> BookRepository { get; }
public MyDomainService(IRepository<Book, Guid> bookRepository)
{
BookRepository = bookRepository;
}
public async Task CreateBooksAsync(List<Book> books)
{
// It'll use bulk inserting automatically.
await BookRepository.InsertManyAsync(books);
}
}
```
> If you use `ExecuteDeleteAsync` or `ExecuteUpdateAsync`, then ABP's soft delete and auditing features can not work. Because these ABP features work with EF Core's change tracking system and these new methods doesn't work with the change tracking system. So, use them carefully.

@ -0,0 +1,208 @@
# Value generation for DDD guarded types with Entity Framework Core 7.0
In domain-driven design (DDD), *guarded keys* can improve the type safety of key properties. This is achieved by wrapping the key type in another type which is specific to the use of the key. In this article, I will explain the cases why you may need to use guarded types and discuss the advantages and limitations when implementing to an ABP application.
> You can find the source code of the example application [here](https://github.com/abpframework/abp-samples/tree/master/EfCoreGuardedTypeDemo).
## The Problem
While developing an applications, there are many cases where we manually assign foreign keys that can be in guid type or integer type, etc. This manual assignment mistakes can cause miss-match of unique identifiers, such as **assigning a product ID to a category**, that can be hard to detect in the future.
Here is a very simplified sample of wrong assignment when trying to update a product category:
````csharp
public class ProductAppService : MyProductStoreAppService, IProductAppService
{
private readonly IRepository<Product, Guid> _productRepository;
public ProductAppService(IRepository<Product, Guid> productRepository)
{
_productRepository = productRepository;
}
public async Task UpdateProductCategoryAsync(Guid productId, Guid categoryId)
{
var productToUpdate = await _productRepository.GetAsync(productId);
productToUpdate.CategoryId = productId; // Wrong assignment that causes error only at run-time
await _productRepository.UpdateAsync(productToUpdate);
}
}
````
While the sample demonstrates a very simple mistake, it is easier to come across similar mistakes when the business logic gets more complex especially when you are using methods with **multiple foreign key arguments**. The next section offers using guarded types to prevent these kind of problems as a solution to the problem.
## The Solution
Strongly-typed IDs (*guarded keys*) is a DDD approach to address this problem. One of the main problems with .NET users was handling the persisting these objects. With EFCore7, key properties can be guarded with type safety seamlessly.
To use guarded keys, update your aggregate root or entity unique identifier with a complex type to overcome *primitive obsession*:
````csharp
public readonly struct CategoryId
{
public CategoryId(Guid value) => Value = value;
public Guid Value { get; }
}
public readonly struct ProductId
{
public ProductId(Guid value) => Value = value;
public Guid Value { get; }
}
````
You can now use these keys for your aggregate roots or entities:
```csharp
public class Product : AggregateRoot<ProductId>
{
public ProductId Id { get; set; }
public string Name { get; set; }
public CategoryId CategoryId { get; set; }
private Product() { }
public Product(ProductId id, string name) : base(id)
{
Name = Check.NotNullOrEmpty(name, nameof(name));
}
}
public class Category : AggregateRoot<CategoryId>
{
public CategoryId Id { get; set; }
public string Name { get; set; }
public List<Product> Products { get; } = new();
private Category() { }
public Category(CategoryId id, string name) : base(id)
{
Name = Check.NotNullOrEmpty(name, nameof(name));
}
}
```
`ProductId` and `CategoryId` guarded key types shown in the sample use `Guid` key values, which means Guid values will be used in the mapped database tables. This is achieved by defining [value converters](https://learn.microsoft.com/en-us/ef/core/modeling/value-conversions) for the types.
Override the `ConfigureConventions` method of your DbContext to use the value converters:
````csharp
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Properties<ProductId>().HaveConversion<ProductIdConverter>();
configurationBuilder.Properties<CategoryId>().HaveConversion<CategoryIdConverter>();
}
private class ProductIdConverter : ValueConverter<ProductId, Guid>
{
public ProductIdConverter()
: base(v => v.Value, v => new(v))
{
}
}
private class CategoryIdConverter : ValueConverter<CategoryId, Guid>
{
public CategoryIdConverter()
: base(v => v.Value, v => new(v))
{
}
}
````
> The code here uses `struct` types. This means they have appropriate value-type semantics for use as keys. If `class` types are used instead, then they need to either override equality semantics or also specify a [value comparer](https://learn.microsoft.com/en-us/ef/core/modeling/value-comparers).
Now, you can use generic (or custom) repositories of ABP using the guarded type as the key for the repository:
```csharp
public class ProductStoreDataSeedContributor : IDataSeedContributor, ITransientDependency
{
private readonly IRepository<Category, CategoryId> _categoryRepository;
private readonly IRepository<Product, ProductId> _productRepository;
public ProductStoreDataSeedContributor(
IRepository<Category, CategoryId> categoryRepository,
IRepository<Product, ProductId> productRepository
)
{
_categoryRepository = categoryRepository;
_productRepository = productRepository;
}
// ...
}
```
You can also use `integer` as guarded type for your key properties and use [Sequence-based key generation for SQL Server](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#sequence-based-key-generation-for-sql-server) for value generation.
## Discussions
In this section, I will discuss the use cases of guarded types and limitations when implementing to an ABP application.
### Do I need to use guarded types?
If you are already following the best practices of [Domain-Driven Design](https://docs.abp.io/en/abp/latest/Domain-Driven-Design), you are aware that **updates** and **creations** of an aggregate are done **over** the aggregate root itself as a whole unit. And entity state changes of an aggregate root should be done using the [domain services](https://docs.abp.io/en/abp/latest/Domain-Services). Domain services should already validate the entity.
**Example: Using domain service to update product:**
````csharp
public class ProductManager : DomainService
{
private readonly IRepository<Product, Guid> _productRepository;
public ProductManager(IRepository<Product, Guid> productRepository)
{
_productRepository = productRepository;
}
public Task<Product> AssignCategory(Product product, Category category)
{
// ...
product.CategoryId = category.Id;
//..
}
}
````
In this sample, domain service validates that both **product** and the **category** entities, passed by the application layer, are valid objects since they are not key properties. However, manual assignment is already in place and more complex the domain logic, higher to miss out mistakes. At the end, it will depend on your tolerance level for developer errors comparing to the time you want to spend time on additional code base for guarded types.
### Limitations
One important limitation is automatic value generation when using `Guid` as guarded type for your key properties. The basic repository can not generate the unique identifier automatically by the time this article is written:
```csharp
public readonly struct ProductId
{
public ProductId(Guid value) => Value = value;
public Guid Value { get; }
}
```
you need to generate the unique identifier **manually**:
````csharp
var newProduct = await _productRepository.InsertAsync(
new Product(new ProductId(_guidGenerator.Create()), "New product")
);
````
## Conclusion
In this article, I tried to explain DDD guarded types for key properties and value generation for these properties using Entity Framework 7.0 and ABP.
Using strongly-typed key properties reduce the chance of unnoticed errors. Admittedly it increases the code complexity by adding new types to your solution with extra coding, especially if you are using classes as keys. Guarded types provide improved safety for your code base at the expense of additional code complexity as in many DDD concepts and patterns.
If you have a large team working on a large scale solution containing complex business logics where key assignments are abundant or if you are using methods with multiple foreign key arguments, I personally suggest using guarded types.
## The Source Code
* You can find the full source code of the example application [here](https://github.com/abpframework/abp-samples/tree/master/EfCoreGuardedTypeDemo).
## See Also
* [What's new in EF Core 7.0](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew)
* [ABP Framework: Domain Driven Design](https://docs.abp.io/en/abp/latest/Domain-Driven-Design)

@ -0,0 +1,231 @@
# Rate Limiting with ASP.NET Core 7.0
Rate limiting is a way of controlling the traffic that a web application or API receives. In other words, rate limiting helps you control the amount of traffic each user has access to at any given time. This is extremely useful when you want to manage the load on your server or services, avoid going over your monthly data transfer limit and allow the system to continue to function and meet service level agreements, even when an increase in demand places an extreme load on resources.
In this article, we will look at what rate limiting is, why we need to use it, how the different rate limiting algorithms provided with .NET 7.0 work, and best practices for using rate limiting in your application.
## What is rate limiting?
Whether accidental or intentional, users may exhaust resources in a way that impacts others. When a number of requests are received on to resources for a long time, the server can run out of those resources. These resources can include memory, threads, connections, or anything else that is limited. To avoid this situation, set rate limiters. Rate limiters control the consumption of resources used by an instance of an application, a user, an individual tenant, or an entire service.
## Why do you need to use rate limiting?
A rate limiting system is crucial in any application where you have to control or throttle user requests or traffic. This is especially true in applications running on a cloud hosting platform because the users traffic can affect the whole server where the application is hosted.
Why do you need to implement rate limiting? Here are a few reasons:
- To ensure that a system continues to meet service level agreements (SLA).
- To prevent a single user, tenant, service, or so on from monopolizing the resources provided by an application.
- To help cost-optimize a system by limiting the maximum resource levels needed to keep it functioning.
## Rate limiter algorithms
The [`RateLimiterOptionsExtensions`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.ratelimiting.ratelimiteroptionsextensions) class provides the following extension methods for rate limiting:
- **[Fixed window](https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet/#fixed-window-limit)**: Fixed-window limits—such as 3,000 requests per hour or 10 requests per day—are easy to state, but they are subject to spikes at the edges of the window, as available quota resets. Consider, for example, a limit of 3,000 requests per hour, which still allows for a spike of all 3,000 requests to be made in the first minute of the hour, which might overwhelm the service.
- [**Sliding window**:](https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet/#sliding-window-limit) Sliding windows have the benefits of a fixed window, but the rolling window of time smoothes out bursts. Systems such as Redis facilitate this technique with expiring keys.
- [**Token bucket**](https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet/#token-bucket-limit): A token bucket maintains a rolling and accumulating budget of usage as a balance of tokens. A token bucket adds tokens at some rate. When a service request is made, the service attempts to withdraw a token (decrementing the token count) to fulfill the request. If there are no tokens in the bucket, the service has reached its limit and responds with backpressure.
- [**Concurrency**](https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?preserve-view=true&view=aspnetcore-7.0#concurrency-limiter): A concurrency limiter is the simplest form of rate limiting. It doesnt look at time, just at number of concurrent requests.
In order to be a more realistic example, instead of making an example with each rate limiter algorithm, we will implement the following three algorithms in an **ABP-based** application.
1. We will add a `SlidingWindowLimiter` with a partition for all anonymous users.
2. We will add a `TokenBucketRateLimiter` with a partition for each authenticated user.
3. We will add a `ConcurrencyLimiter` with a partition for each Tenant.
**Note:** The following sample isn't meant for production code but is an example of how to use the limiters in ABP-based applications.
### Limiter with `OnRejected`, `RetryAfter`, and `GlobalLimiter`
#### Add rate limiter
Let's create the following method in the `MyProjectNameWebModule.cs` class in the `MyProjectName.Web` project.
**Note:** If the `**.Web` project is not in your application, you can do the same in the project where your application is hosted.
```csharp
private void ConfigureRateLimiters(ServiceConfigurationContext context)
{
context.Services.AddRateLimiter(limiterOptions =>
{
limiterOptions.OnRejected = (context, cancellationToken) =>
{
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
{
context.HttpContext.Response.Headers.RetryAfter =
((int) retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo);
}
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
context.HttpContext.RequestServices.GetService<ILoggerFactory>()?
.CreateLogger("Microsoft.AspNetCore.RateLimitingMiddleware")
.LogWarning("OnRejected: {RequestPath}", context.HttpContext.Request.Path);
return new ValueTask();
};
limiterOptions.AddPolicy("UserBasedRateLimiting", context =>
{
var currentUser = context.RequestServices.GetService<ICurrentUser>();
if (currentUser is not null && currentUser.IsAuthenticated)
{
return RateLimitPartition.GetTokenBucketLimiter(currentUser.UserName, _ => new TokenBucketRateLimiterOptions
{
TokenLimit = 10,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 3,
ReplenishmentPeriod = TimeSpan.FromMinutes(1),
TokensPerPeriod = 4,
AutoReplenishment = true
});
}
return RateLimitPartition.GetSlidingWindowLimiter("anonymous-user",
_ => new SlidingWindowRateLimiterOptions
{
PermitLimit = 2,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1,
Window = TimeSpan.FromMinutes(1),
SegmentsPerWindow = 2
});
});
limiterOptions.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
{
var currentTenant = context.RequestServices.GetService<ICurrentTenant>();
if (currentTenant is not null && currentTenant.IsAvailable)
{
return RateLimitPartition.GetConcurrencyLimiter(currentTenant!.Name, _ => new ConcurrencyLimiterOptions
{
PermitLimit = 5,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 1
});
}
return RateLimitPartition.GetNoLimiter("host");
});
});
}
```
In the above example, the `TokenBucketLimiter` is used for each authenticated user, while the `SlidingWindowLimiter` is used for all anonymous users. Additionally, as a global limiter, the `ConcurrencyLimiter` is used for each tenant, while rate limiting is disabled for the host(tenant is not available). Also, for requests that are rejected when the limit is reached, sets the response status code to [429 Too Many Requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) and the response mentions when to retry (if available from the rate-limiting metadata).
Let's call the `ConfigureRateLimiters` method that we created in the `ConfigureServices` method.
The final version of the `ConfigureServices` method:
```csharp
public override void ConfigureServices(ServiceConfigurationContext context)
{
var hostingEnvironment = context.Services.GetHostingEnvironment();
var configuration = context.Services.GetConfiguration();
ConfigureBundles();
ConfigureUrls(configuration);
ConfigurePages(configuration);
ConfigureAuthentication(context);
ConfigureImpersonation(context, configuration);
ConfigureAutoMapper();
ConfigureVirtualFileSystem(hostingEnvironment);
ConfigureNavigationServices();
ConfigureAutoApiControllers();
ConfigureSwaggerServices(context.Services);
ConfigureExternalProviders(context);
ConfigureHealthChecks(context);
ConfigureCookieConsent(context);
ConfigureTheme();
Configure<PermissionManagementOptions>(options =>
{
options.IsDynamicPermissionStoreEnabled = true;
});
ConfigureRateLimiters(context); // added
}
```
#### Add RateLimiter middleware
Add the following line just before the `app.UseConfiguredEndpoints(...)` line to add the `RateLimiter` middleware to your ASP.NET Core request pipeline:
```csharp
app.UseRateLimiter();
```
#### Use rate limiter for all controllers
Let's edit the `ConfiguredEndpoints` middleware as follows:
```csharp
app.UseConfiguredEndpoints(endpoints =>
{
endpoints.MapRazorPages()
.DisableRateLimiting();
endpoints.MapControllers()
.RequireRateLimiting("UserBasedRateLimiting");
});
```
- **DisableRateLimiting:** It is used to disable the `ConcurrencyLimiter` for razor pages, which we set globally when the tenant is available.
- **RequireRateLimiting:** We have enabled the rate limiter, which we define according to whether the user is authenticated or not, for all controllers.
## `EnableRateLimiting` and `DisableRateLimiting` attributes
It's kind of unrealistic to always use rate limiting for all controllers or pages. Sometimes, we may want to throttle a particular endpoint or page. In such cases, we can use the `EnableRateLimiting` and `DisableRateLimiting` attributes. The `EnableRateLimiting` and `DisableRateLimiting` attributes can be applied to a controller, action method, or razor rage. Check [here](https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?preserve-view=true&view=aspnetcore-7.0#enableratelimiting-and-disableratelimiting-attributes) for more.
## Rate limit an HTTP handler
Rate limiting when sending an HTTP request can be a good practice, especially in service-to-service communication. Because, resources are consumed by apps that rely on them, and when an app makes too many requests for a single resource, it can lead to *resource contention*. Resource contention occurs when a resource is consumed by too many clients, and the resource is unable to serve all of the apps that are requesting it. This can result in a poor user experience, and in some cases, it can even lead to a denial of service (DoS) attack. Since there are similar codes, I will not mention an example in this article, but to avoid such situations, you can write your own HTTP handler as [here](https://learn.microsoft.com/en-us/dotnet/core/extensions/http-ratelimiter#implement-a-delegatinghandler-subclass).
## How does it work?
[System.Threading.RateLimiting](https://www.nuget.org/packages/System.Threading.RateLimiting) provides the primitives for writing rate limiters as well as providing a few commonly used algorithms built-in. The main type is the abstract base class [RateLimiter](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Threading.RateLimiting/src/System/Threading/RateLimiting/RateLimiter.cs).
```csharp
public abstract class RateLimiter : IAsyncDisposable, IDisposable
{
public abstract int GetAvailablePermits();
public abstract TimeSpan? IdleDuration { get; }
public RateLimitLease Acquire(int permitCount = 1);
public ValueTask<RateLimitLease> WaitAsync(int permitCount = 1, CancellationToken cancellationToken = default);
public void Dispose();
public ValueTask DisposeAsync();
}
```
`RateLimiter` contains `Acquire` and `WaitAsync` as the core methods for trying to gain permits for a resource that is being protected. Depending on the application, the protected resource may need to acquire more than 1 permits, so `Acquire` and `WaitAsync` both accept an optional `permitCount` parameter. `Acquire` is a synchronous method that will check if enough permits are available or not and return a `RateLimitLease` which contains information about whether you successfully acquired the permits or not. `WaitAsync` is similar to `Acquire` except that it can support queuing permit requests which can be de-queued at some point in the future when the permits become available, which is why its asynchronous and accepts an optional `CancellationToken` to allow canceling the queued request.
`RateLimitLease` has an `IsAcquired` property which is used to see if the permits were acquired. Additionally, the `RateLimitLease` may contain metadata such as a suggested retry-after period if the lease failed. Finally, the `RateLimitLease` is disposable and should be disposed when the code is done using the protected resource. The disposal will let the `RateLimiter` know to update its limits based on how many permits were acquired.
## Limitations
In most cases, the rate-limiting middleware provided with ASP.NET 7.0 will meet your requirements. However, if you would want to return statistics about your limits (e.g. [the way GitHub does](https://docs.github.com/en/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28#rate-limit-http-headers)), youll find out that the ASP.NET rate limiting middleware does not support this. You wont have access to the “number of requests remaining” or other metadata. Not in `OnRejected`, and definitely not if you want to return this data as headers on every request.
## Best practices for rate limiting
In order to use rate limiting properly, you need to have a solid understanding of the types of limiting available, as well as the data rate and data volume of your service. You also need to have a clear idea of how many users you expect to use your service as well as how they will interact with it. The best practices for rate limiting are as follows:
- Find the right rate limiter algorithm for your endpoint. I mean, the cost of an endpoint should be considered when selecting a limiter. The cost of an endpoint includes the resources used, for example, time, data access, CPU, and I/O.
- Set realistic limits. Once youve figured out all the above, you need to set realistic limits for each service. Then, before deploying an app using rate limiting to production, stress test the app to validate the rate limiters and options used. For example, create a [JMeter script](https://jmeter.apache.org/usermanual/jmeter_proxy_step_by_step.html) with a tool like [BlazeMeter](https://guide.blazemeter.com/hc/articles/207421695-Writing-your-first-JMeter-script) or [Apache JMeter HTTP(S) Test Script Recorder](https://jmeter.apache.org/usermanual/jmeter_proxy_step_by_step.html) and load the script to [Azure Load Testing](https://learn.microsoft.com/en-us/azure/load-testing/overview-what-is-azure-load-testing).
- In response to rate-limiting, intermittent, or non-specific errors, a client should generally retry the request after a delay. It is a best practice for this delay to increase exponentially after each failed request, which is referred to as *exponential backoff*. When many clients might be making schedule-based requests (such as fetching results every hour), additional random time (*jitter*) should be applied to the request timing, the backoff period, or both of them to ensure that these multiple client instances don't become periodic [thundering herd](https://www.wikiwand.com/en/Thundering_herd_problem), and cause a form of DDoS themselves.
## Conclusion
In this article, weve covered what rate limiting is, why you need to use it and the best practices for doing so. Weve also looked at how to use three rate-limiting algorithms that are provided with .NET 7.0 on ABP-based applications and how rate-limiting works. Now that youre familiar with the concept of rate limiting, its time to start implementing rate limiting in your application. This will allow you to control the traffic and ensure that your application is running smoothly without any issues.
## References
- https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?preserve-view=true&view=aspnetcore-7.0
- https://aws.amazon.com/builders-library/timeouts-retries-and-backoff-with-jitter/
- https://blog.maartenballiauw.be/post/2022/09/26/aspnet-core-rate-limiting-middleware.html
- https://learn.microsoft.com/en-us/dotnet/core/extensions/http-ratelimiter
- https://learn.microsoft.com/en-us/azure/architecture/patterns/rate-limiting-pattern
- https://learn.microsoft.com/en-us/azure/architecture/patterns/throttling
- https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet
- https://cloud.google.com/architecture/rate-limiting-strategies-techniques

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 KiB

@ -2,7 +2,7 @@
ABP's Dependency Injection system is developed based on Microsoft's [dependency injection extension](https://medium.com/volosoft/asp-net-core-dependency-injection-best-practices-tips-tricks-c6e9c67f9d96) library (Microsoft.Extensions.DependencyInjection nuget package). So, it's documentation is valid in ABP too.
> While ABP has no core dependency to any 3rd-party DI provider, it's required to use a provider that supports dynamic proxying and some other advanced features to make some ABP features properly work. Startup templates come with Autofac installed. See [Autofac integration](Autofac-Integration.md) document for more information.
> While ABP has no core dependency to any 3rd-party DI provider. However, it's required to use a provider that supports dynamic proxying and some other advanced features to make some ABP features properly work. Startup templates come with [Autofac](https://autofac.org/) installed. See [Autofac integration](Autofac-Integration.md) document for more information.
## Modularity
@ -20,24 +20,24 @@ public class BlogModule : AbpModule
## Conventional Registration
ABP introduces conventional service registration. You need not do anything to register a service by convention. It's automatically done. If you want to disable it, you can set `SkipAutoServiceRegistration` to `true` by overriding the `PreConfigureServices` method.
ABP introduces conventional service registration. You need not do anything to register a service by convention. It's automatically done. If you want to disable it, you can set `SkipAutoServiceRegistration` to `true` in the constructor of your module class. Example:
````C#
public class BlogModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
public BlogModule()
{
SkipAutoServiceRegistration = true;
}
}
````
Once you skip auto registration, you should manually register your services. In that case, ``AddAssemblyOf`` extension method can help you to register all your services by convention. Example:
Once you skip the auto registration, you should manually register your services. In that case, ``AddAssemblyOf`` extension method can help you to register all your services by convention. Example:
````c#
public class BlogModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
public BlogModule()
{
SkipAutoServiceRegistration = true;
}
@ -105,9 +105,7 @@ Example:
[Dependency(ServiceLifetime.Transient, ReplaceServices = true)]
public class TaxCalculator
{
}
````
``Dependency`` attribute has a higher priority than other dependency interfaces if it defines the ``Lifetime`` property.
@ -120,7 +118,6 @@ public class TaxCalculator
[ExposeServices(typeof(ITaxCalculator))]
public class TaxCalculator: ICalculator, ITaxCalculator, ICanCalculate, ITransientDependency
{
}
````
@ -179,7 +176,11 @@ public class MyModule : AbpModule
public override void ConfigureServices(ServiceConfigurationContext context)
{
//Replacing the IConnectionStringResolver service
context.Services.Replace(ServiceDescriptor.Transient<IConnectionStringResolver, MyConnectionStringResolver>());
context.Services.Replace(
ServiceDescriptor.Transient<
IConnectionStringResolver,
MyConnectionStringResolver
>());
}
}
````
@ -202,7 +203,7 @@ public class TaxAppService : ApplicationService
_taxCalculator = taxCalculator;
}
public void DoSomething()
public async Task DoSomethingAsync()
{
//...use _taxCalculator...
}
@ -227,7 +228,7 @@ public class MyService : ITransientDependency
Logger = NullLogger<MyService>.Instance;
}
public void DoSomething()
public async Task DoSomethingAsync()
{
//...use Logger to write logs...
}
@ -244,17 +245,21 @@ One restriction of property injection is that you cannot use the dependency in y
Property injection is also useful when you want to design a base class that has some common services injected by default. If you're going to use constructor injection, all derived classes should also inject depended services into their own constructors which makes development harder. However, be very careful using property injection for non-optional services as it makes it harder to clearly see the requirements of a class.
#### DisablePropertyInjectionAttribute
#### DisablePropertyInjection Attribute
You can use `[DisablePropertyInjection]` attribute on class or properties to disable property injection for the whole class or some specific properties.
You can use `[DisablePropertyInjection]` attribute on classes or their properties to disable property injection for the whole class or some specific properties.
````C#
// Disabling for all properties of the MyService class
[DisablePropertyInjection]
public class MyService : ITransientDependency
{
public ILogger<MyService> Logger { get; set; }
public ITaxCalculator TaxCalculator { get; set; }
}
// Disabling only for the TaxCalculator property
public class MyService : ITransientDependency
{
public ILogger<MyService> Logger { get; set; }
@ -262,17 +267,21 @@ public class MyService : ITransientDependency
[DisablePropertyInjection]
public ITaxCalculator TaxCalculator { get; set; }
}
````
### Resolve Service from IServiceProvider
You may want to resolve a service directly from ``IServiceProvider``. In that case, you can inject IServiceProvider into your class and use ``GetService`` method as shown below:
You may want to resolve a service directly from ``IServiceProvider``. In that case, you can inject `IServiceProvider` into your class and use the ``GetService`` or the `GetRequiredService` method as shown below:
````C#
public class MyService : ITransientDependency
{
public ILogger<MyService> Logger { get; set; }
private readonly ITaxCalculator _taxCalculator;
public MyService(IServiceProvider serviceProvider)
{
_taxCalculator = serviceProvider.GetRequiredService<ITaxCalculator>();
}
}
````
@ -374,15 +383,15 @@ IEnumerable<IExternalLogger> services = _serviceProvider.GetServices<IExternalLo
### Releasing/Disposing Services
If you used a constructor or property injection, you don't need to be concerned about releasing the service's resources. However, if you have resolved a service from ``IServiceProvider``, you might, in some cases, need to take care about releasing the service resources.
If you used a constructor or property injection, you don't need to be concerned about releasing the service's resources. However, if you have resolved a service from ``IServiceProvider``, in some cases, you might need to take care about releasing the service resources.
ASP.NET Core releases all services at the end of a current HTTP request, even if you directly resolved from ``IServiceProvider`` (assuming you injected IServiceProvider). But, there are several cases where you may want to release/dispose manually resolved services:
ASP.NET Core releases all services at the end of a current HTTP request, even if you directly resolved from ``IServiceProvider`` (assuming you injected `IServiceProvider`). But, there are several cases where you may want to release/dispose manually resolved services:
* Your code is executed outside of AspNet Core request and the executer hasn't handled the service scope.
* Your code is executed outside of ASP.NET Core request and the executer hasn't handled the service scope.
* You only have a reference to the root service provider.
* You may want to immediately release & dispose services (for example, you may creating too many services with big memory usage and don't want to overuse memory).
* You may want to immediately release & dispose services (for example, you may creating too many services with big memory usages and don't want to overuse the memory).
In any case, you can use such a 'using' code block to safely and immediately release services:
In any case, you can create a service scope block to safely and immediately release services:
````C#
using (var scope = _serviceProvider.CreateScope())
@ -392,7 +401,42 @@ using (var scope = _serviceProvider.CreateScope())
}
````
Both services are released when the created scope is disposed (at the end of the using block).
Both services are released when the created scope is disposed (at the end of the `using` block).
### Cached Service Providers
ABP provides two special services to optimize resolving services from `IServiceProvider`. `ICachedServiceProvider` and `ITransientCachedServiceProvider` both inherits from the `IServiceProvider` interface and internally caches the resolved services, so you get the same service instance even if you resolve a service multiple times.
The main difference is the `ICachedServiceProvider` is itself registered as scoped, while the `ITransientCachedServiceProvider` is registered as transient to the dependency injection system.
The following example injects the `ICachedServiceProvider` service and resolves a service in the `DoSomethingAsync` method:
````csharp
public class MyService : ITransientDependency
{
private readonly ICachedServiceProvider _serviceProvider;
public MyService(ICachedServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task DoSomethingAsync()
{
var taxCalculator = _serviceProvider.GetRequiredService<ITaxCalculator>();
// TODO: Use the taxCalculator
}
}
````
With such a usage, you don't need to deal with creating service scopes and disposing the resolved services (as explained in the *Releasing/Disposing Services* section above). Because all the services resolved from the `ICachedServiceProvider` will be released once the service scope of the `MyService` instance is disposed. Also, you don't need to care about memory leaks (because of creating too many `ITaxCalculator` instances if we call `DoSomethingAsync` too many times), because only one `ITaxCalculator` instance is created, and it is reused.
Since `ICachedServiceProvider` and `ITransientCachedServiceProvider` extends the standard `IServiceProvider` interface, you can use all the extension method of the `IServiceProvider` interface on them. In addition, they provides some other methods to provide a default value or a factory method for the services that are not found (that means not registered to the dependency injection system). Notice that the default value (or the value returned from your factory method) is also cached and reused.
Use `ICachedServiceProvider` (instead of `ITransientCachedServiceProvider`) unless you need to create the service cache per usage. `ITransientCachedServiceProvider` guarantees that the created service instances are not shared with any other service, even they are in the same service scope. The services resolved from `ICachedServiceProvider` are shared with other services in the same service scope (in the same HTTP Request, for example), so it can be thought as more optimized.
> ABP Framework also provides the `IAbpLazyServiceProvider` service. It does exists for backward compatibility and works exactly same with the `ITransientCachedServiceProvider` service. So, use the `ITransientCachedServiceProvider` since the `IAbpLazyServiceProvider` might be removed in future ABP versions.
## Advanced Features

@ -0,0 +1,100 @@
# Configuring for Production
ABP Framework has a lot of options to configure and fine-tune its features. They are all explained in their own documents. Default values for these options are pretty well for most of the deployment environments. However, you may need to care about some options based on how you've structured your deployment environment. In this document, we will highlight these kind of options. So, it is highly recommended to read this document in order to not have unexpected behaviors in your system in production.
## Distributed Cache Prefix
ABP's [distributed cache infrastructure](../Caching.md) provides an option to set a key prefix for all of your data that is saved into your distributed cache provider. The default value of this option is not set (it is `null`). If you are using a distributed cache server that is shared by different applications, then you can set a prefix value to isolate an application's cache data from others.
````csharp
Configure<AbpDistributedCacheOptions>(options =>
{
options.KeyPrefix = "MyCrmApp";
});
````
That's all. ABP, then will add this prefix to all of your cache keys in your application as along as you use ABP's `IDistributedCache<TCacheItem>` or `IDistributedCache<TCacheItem,TKey>` services. See the [Caching documentation](../Caching.md) if you are new to distributed caching.
> **Warning**: If you use ASP.NET Core's standard `IDistributedCache` service, it's your responsibility to add the key prefix (you can get the value by injecting `IOptions<AbpDistributedCacheOptions>`). ABP can not do it.
> **Warning**: Even if you have never used distributed caching in your own codebase, ABP still uses it for some features. So, you should always configure this prefix if your caching server is shared among multiple systems.
> **Warning**: If you are building a microservice system, then you will have multiple applications that share the same distributed cache server. In such systems, all applications (or services) should normally use the same cache prefix, because you want all the applications to use the same cache data to have consistency between them.
> **Warning**: Some of ABP's startup templates are pre-configured to set a prefix value for the distributed cache. So, please check your application code if it is already configured.
## Distributed Lock Prefix
ABP's [distributed locking infrastructure](../Distributed-Locking.md) provides an option to set a prefix for all the keys you are using in the distributed lock server. The default value of this option is not set (it is `null`). If you are using a distributed lock server that is shared by different applications, then you can set a prefix value to isolate an application's lock from others.
````csharp
Configure<AbpDistributedLockOptions>(options =>
{
options.KeyPrefix = "MyCrmApp";
});
````
That's all. ABP, then will add this prefix to all of your keys in your application. See the [Distributed Locking documentation](../Distributed-Locking.md) if you are new to distributed locking.
> **Warning**: Even if you have never used distributed locking in your own codebase, ABP still uses it for some features. So, you should always configure this prefix if your distributed lock server is shared among multiple systems.
> **Warning**: If you are building a microservice system, then you will have multiple applications that share the same distributed locking server. In such systems, all applications (or services) should normally use the same lock prefix, because you want to globally lock your resources in your system.
> **Warning**: Some of ABP's startup templates are pre-configured to set a prefix value for distributed locking. So, please check your application code if it is already configured.
## Email Sender
ABP's [Email Sending](../Emailing.md) system abstracts sending emails from your application and module code and allows you to configure the email provider and settings in a single place.
Email service is configured to write email contents to the standard [application log](../Logging.md) in development environment. You should configure the email settings to be able to send emails to users in your production environment.
Please see the [Email Sending](../Emailing.md) document to learn how to configure its settings to really send emails.
> **Warning**: If you don't configure the email settings, you will get errors while trying to send emails. For example, the [Account module](../Modules/Account.md)'s *Password Reset* feature sends email to the users to reset their passwords if they forget it.
## SMS Sender
ABP's [SMS Sending abstraction](https://docs.abp.io/en/abp/latest/SMS-Sending) provides a unified interface to send SMS to users. However, its implementation is left to you. Because, typically a paid SMS service is used to send SMS, and ABP doesn't depend on a specific SMS provider.
So, if you are using the `ISmsSender` service, you must implement it yourself, as shown in the following code block:
````csharp
public class MySmsSender : ISmsSender, ITransientDependency
{
public async Task SendAsync(SmsMessage smsMessage)
{
// TODO: Send it using your provider...
}
}
````
> [ABP Commercial](https://commercial.abp.io/) provides a [Twilio SMS Module](https://docs.abp.io/en/commercial/latest/modules/twilio-sms) as a pre-built integration with the popular [Twilio](https://www.twilio.com/) platform.
## BLOB Provider
If you use ABP's [BLOB Storing](https://docs.abp.io/en/abp/latest/Blob-Storing) infrastructure, you should care about the BLOB provider in your production environment. For example, if you use the [File System](../Blob-Storing-File-System.md) provider and your application is running in a Docker container, you should configure a volume mapping for the BLOB storage path. Otherwise, your data will be lost when the container is restarted. Also, the File System is not a good provider for production if you have a [clustered deployment](Clustered-Environment.md) or a microservice system.
Check the [BLOB Storing](../Blob-Storing.md) document to see all the available BLOB storage providers.
> **Warning**: Even if you don't directly use the BLOB Storage system, a module you are depending on may use it. For example, ABP Commercial's [File Management](https://docs.abp.io/en/commercial/latest/modules/file-management) module stores file contents, and the [Account](https://docs.abp.io/en/commercial/latest/modules/account) module stores user profile pictures in the BLOB Storage system. So, be careful with the BLOB Storing configuration in production. Note that ABP Commercial uses the [Database Provider](../Blob-Storing-Database.md) as a pre-configured BLOB storage provider, which works in production without any problem, but you may still want to use another provider.
## String Encryption
ABP's [`IStringEncryptionService` Service](../String-Encryption.md) simply encrypts and decrypts given strings based on a password phrase. You should configure the `AbpStringEncryptionOptions` options for the production with a strong password and keep it as a secret. You can also configure the other properties of those options class. See the following example:
````csharp
Configure<AbpStringEncryptionOptions>(options =>
{
options.DefaultPassPhrase = "gs5nTT042HAL4it1";
});
````
Note that ABP CLI automatically sets the password to a random value on a new project creation. However, it is stored in the `appsettings.json` file and is generally added to your source control. It is suggested to use [User Secrets](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets) or [Environment Variables](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration) to set that value.
## The Swagger UI
ABP's startup solution templates come with [Swagger UI](https://swagger.io/) pre-installed. Swagger is a pretty standard and useful tool to discover and test your HTTP APIs on a built-in UI that is embedded into your application or service. It is typically used in development environment, but you may want to enable it on staging or production environments too.
While you will always secure your HTTP APIs with other techniques (like the [Authorization](../Authorization.md) system), allowing malicious software and people to easily discover your HTTP API endpoint details can be considered as a security problem for some systems. So, be careful while taking the decision of enabling or disabling Swagger for the production environment.
> You may also want to see the [ABP Swagger integration](../API/Swagger-Integration.md) document.

@ -6,4 +6,5 @@ However, there are some topics that you should care about when you are deploying
## Guides
* [Configuring for production](Configuring-Production.md): Notes for some important configurations for production environments.
* [Deploying to a clustered environment](Clustered-Environment.md): Explains how to configure your application when you want to run multiple instances of your application concurrently.

@ -434,6 +434,29 @@ Here, the following properties are available on the `config` object:
* `HandlerSelector`: A predicate to filter the event handled types (classes implementing the `IDistributedEventHandler<TEvent>` interface) to be used for this configuration. This is especially useful if you want to ignore some event handler types from inbox processing, or want to define named inbox configurations and group event handlers within these configurations. See the *Named Configurations* section.
* `ImplementationType`: Type of the class that implements the database operations for the inbox. This is normally set when you call `UseDbContext` as shown before. See *Implementing a Custom Outbox/Inbox Database Provider* section for advanced usages.
#### AbpEventBusBoxesOptions
`AbpEventBusBoxesOptions` can be used to fine-tune how inbox and outbox systems work. For most of the systems, using the defaults would be more than enough, but you can configure it to optimize your system when it is needed.
Just like all the [options classes](Options.md), `AbpEventBusBoxesOptions` can be configured in the `ConfigureServices` method of your [module class](Module-Development-Basics.md) as shown in the following code block:
````csharp
Configure<AbpEventBusBoxesOptions>(options =>
{
// TODO: configure the options
});
````
`AbpEventBusBoxesOptions` has the following properties to be configured:
* `BatchPublishOutboxEvents`: Can be used to enable or disable batch publishing events to the message broker. Batch publishing works if it is supported by the distributed event bus provider. If not supported, events are sent one by one as the fallback logic. Keep it as enabled since it has a great performance gain wherever possible. Default value is `true` (enabled).
* `PeriodTimeSpan`: The period of the inbox and outbox message processors to check if there is a new event in the database. Default value is 2 seconds (`TimeSpan.FromSeconds(2)`).
* `CleanOldEventTimeIntervalSpan`: The event inbox system periodically checks and deletes the old processed events from the inbox in the database. You can set this value to determine the check period. Default value is 6 hours (`TimeSpan.FromHours(6)`).
* `WaitTimeToDeleteProcessedInboxEvents`: Inbox events are not deleted from the database for a while even if they are successfully processed. This is for a system to prevent multiple process of the same event (if the event broker sends it twice). This configuration value determines the time to keep the processed events. Default value is 2 hours (`TimeSpan.FromHours(2)`).
* `InboxWaitingEventMaxCount`: The maximum number of events to query at once from the inbox in the database. Default value is 1000.
* `OutboxWaitingEventMaxCount`: The maximum number of events to query at once from the outbox in the database. Default value is 1000.
* `DistributedLockWaitDuration`: ABP uses [distributed locking](Distributed-Locking.md) to prevent concurrent access to the inbox and outbox messages in the database, when running multiple instance of the same application. If an instance of the application can not obtain the lock, it tries after a duration. This is the configuration of that duration. Default value is 15 seconds (`TimeSpan.FromSeconds(15)`).
### Skipping Outbox
`IDistributedEventBus.PublishAsync` method provides an optional parameter, `useOutbox`, which is set to `true` by default. If you bypass outbox and immediately publish an event, you can set it to `false` for a specific event publishing operation.

@ -101,3 +101,11 @@ See https://github.com/abpframework/abp/pull/13644 for more info.
We've already done this for our themes.
See https://github.com/abpframework/abp/pull/13845 for more info.
## Replaced `BlogPostPublicDto` with `BlogPostCommonDto`
- In the CMS Kit Module, `BlogPostPublicDto` has been moved to `Volo.CmsKit.Common.Application.Contracts` from `Volo.CmsKit.Public.Application.Contracts` and renamed to `BlogPostCommonDto`.
- See the [PR#13499](https://github.com/abpframework/abp/pull/13499) for more information.
> You can ignore this if you don't use CMS Kit Module.

@ -11,7 +11,7 @@ See the documents below for the details:
## Extensible Table Component
Using [ngx-datatable](https://github.com/swimlane/ngx-datatable) in extensinble table.
Using [ngx-datatable](https://github.com/swimlane/ngx-datatable) in extensible table.
````ts
<abp-extensible-table

@ -54,9 +54,9 @@ Assuming the `HelloWelcomeMessage` is localized as `Hello {0}, welcome!`, both o
## Other Properties & Methods
### abp.localization.values
### abp.localization.resources
`abp.localization.values` property stores all the localization resources, keys and their values.
`abp.localization.resources` property stores all the localization resources, keys and their values.
### abp.localization.isLocalized

@ -614,6 +614,10 @@
{
"text": "Application Configuration",
"path": "API/Application-Configuration.md"
},
{
"text": "Application Localization",
"path": "API/Application-Localization.md"
}
]
},
@ -1299,6 +1303,10 @@
"text": "Deployment",
"path": "Deployment/Index.md",
"items": [
{
"text": "Configuring for Production",
"path": "Deployment/Configuring-Production.md"
},
{
"text": "Deploying to a Clustered Environment",
"path": "Deployment/Clustered-Environment.md"

@ -54,7 +54,7 @@ public class MyLogWorker : HangfireBackgroundWorkerBase
CronExpression = Cron.Daily();
}
public override Task DoWorkAsync()
public override Task DoWorkAsync(CancellationToken cancellationToken = default)
{
Logger.LogInformation("Executed MyLogWorker..!");
return Task.CompletedTask;
@ -69,14 +69,7 @@ public class MyLogWorker : HangfireBackgroundWorkerBase
### UnitOfWork
使用 `UnitOfWorkAttribute` 你需要为工作者定义一个接口:
```csharp
public interface IMyLogWorker : IHangfireBackgroundWorker
{
}
[ExposeServices(typeof(IMyLogWorker))]
public class MyLogWorker : HangfireBackgroundWorkerBase, IMyLogWorker
{
public MyLogWorker()
@ -85,11 +78,13 @@ public class MyLogWorker : HangfireBackgroundWorkerBase, IMyLogWorker
CronExpression = Cron.Daily();
}
[UnitOfWork]
public override Task DoWorkAsync()
public override Task DoWorkAsync(CancellationToken cancellationToken = default)
{
Logger.LogInformation("Executed MyLogWorker..!");
return Task.CompletedTask;
using (var uow = LazyServiceProvider.LazyGetRequiredService<IUnitOfWorkManager>().Begin())
{
Logger.LogInformation("Executed MyLogWorker..!");
return Task.CompletedTask;
}
}
}
```
@ -106,9 +101,6 @@ public class MyModule : AbpModule
ApplicationInitializationContext context)
{
await context.AddBackgroundWorkerAsync<MyLogWorker>();
//如果定义了接口
//await context.AddBackgroundWorkerAsync<IMyLogWorker>();
}
}
````
@ -127,4 +119,4 @@ context.ServiceProvider
它解析给定的后台工作者并添加到 `IBackgroundWorkerManager`.
虽然我们通常在 `OnApplicationInitializationAsync` 中添加后台工作者, 但对此没有限制. 你可以在任何地方注入 `IBackgroundWorkerManager` 并在运行时添加后台工作者.
虽然我们通常在 `OnApplicationInitializationAsync` 中添加后台工作者, 但对此没有限制. 你可以在任何地方注入 `IBackgroundWorkerManager` 并在运行时添加后台工作者.

@ -0,0 +1,113 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
using Pages.Abp.MultiTenancy.ClientProxies;
using Volo.Abp.AspNetCore.Mvc.MultiTenancy;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Threading;
using DependencyAttribute = Volo.Abp.DependencyInjection.DependencyAttribute;
namespace Volo.Abp.AspNetCore.Components.MauiBlazor;
[Dependency(ReplaceServices = true)]
public class MauiBlazorRemoteTenantStore : ITenantStore, ITransientDependency
{
protected AbpTenantClientProxy TenantAppService { get; }
protected IDistributedCache<TenantConfiguration> Cache { get; }
public MauiBlazorRemoteTenantStore(AbpTenantClientProxy tenantAppService, IDistributedCache<TenantConfiguration> cache)
{
TenantAppService = tenantAppService;
Cache = cache;
}
public async Task<TenantConfiguration> FindAsync(string name)
{
var cacheKey = CreateCacheKey(name);
var tenantConfiguration = await Cache.GetOrAddAsync(
cacheKey,
async () => CreateTenantConfiguration(await TenantAppService.FindTenantByNameAsync(name)),
() => new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow =
TimeSpan.FromMinutes(5) //TODO: Should be configurable.
}
);
return tenantConfiguration;
}
public async Task<TenantConfiguration> FindAsync(Guid id)
{
var cacheKey = CreateCacheKey(id);
var tenantConfiguration = await Cache.GetOrAddAsync(
cacheKey,
async () => CreateTenantConfiguration(await TenantAppService.FindTenantByIdAsync(id)),
() => new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow =
TimeSpan.FromMinutes(5) //TODO: Should be configurable.
}
);
return tenantConfiguration;
}
public TenantConfiguration Find(string name)
{
var cacheKey = CreateCacheKey(name);
var tenantConfiguration = Cache.GetOrAdd(
cacheKey,
() => AsyncHelper.RunSync(async () => CreateTenantConfiguration(await TenantAppService.FindTenantByNameAsync(name))),
() => new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow =
TimeSpan.FromMinutes(5) //TODO: Should be configurable.
}
);
return tenantConfiguration;
}
public TenantConfiguration Find(Guid id)
{
var cacheKey = CreateCacheKey(id);
var tenantConfiguration = Cache.GetOrAdd(
cacheKey,
() => AsyncHelper.RunSync(async () => CreateTenantConfiguration(await TenantAppService.FindTenantByIdAsync(id))),
() => new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow =
TimeSpan.FromMinutes(5) //TODO: Should be configurable.
}
);
return tenantConfiguration;
}
protected virtual TenantConfiguration CreateTenantConfiguration(FindTenantResultDto tenantResultDto)
{
if (!tenantResultDto.Success || tenantResultDto.TenantId == null)
{
return null;
}
return new TenantConfiguration(tenantResultDto.TenantId.Value, tenantResultDto.Name);
}
protected virtual string CreateCacheKey(string tenantName)
{
return $"RemoteTenantStore_Name_{tenantName}";
}
protected virtual string CreateCacheKey(Guid tenantId)
{
return $"RemoteTenantStore_Id_{tenantId:N}";
}
}

@ -14,10 +14,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Blazorise" Version="1.1.3.1" />
<PackageReference Include="Blazorise.DataGrid" Version="1.1.3.1" />
<PackageReference Include="Blazorise.Snackbar" Version="1.1.3.1" />
<PackageReference Include="Blazorise.Components" Version="1.1.3.1" />
<PackageReference Include="Blazorise" Version="1.1.4.1" />
<PackageReference Include="Blazorise.DataGrid" Version="1.1.4.1" />
<PackageReference Include="Blazorise.Snackbar" Version="1.1.4.1" />
<PackageReference Include="Blazorise.Components" Version="1.1.4.1" />
</ItemGroup>
</Project>

@ -111,7 +111,7 @@ public class NewCommand : ProjectCreationCommandBase, IConsoleCommand, ITransien
var skipBundling = commandLineArgs.Options.ContainsKey(Options.SkipBundling.Long) || commandLineArgs.Options.ContainsKey(Options.SkipBundling.Short);
if (!skipBundling)
{
await RunBundleForBlazorWasmTemplateAsync(projectArgs);
await RunBundleForBlazorWasmOrMauiBlazorTemplateAsync(projectArgs);
}
await ConfigurePwaSupportForAngular(projectArgs);

@ -118,7 +118,7 @@ public abstract class ProjectCreationCommandBase
Logger.LogInformation("DBMS: " + databaseManagementSystem);
}
var uiFramework = GetUiFramework(commandLineArgs);
var uiFramework = GetUiFramework(commandLineArgs, template);
if (uiFramework != UiFramework.NotSpecified)
{
Logger.LogInformation("UI Framework: " + uiFramework);
@ -413,22 +413,24 @@ public abstract class ProjectCreationCommandBase
}
}
protected async Task RunBundleForBlazorWasmTemplateAsync(ProjectBuildArgs projectArgs)
protected async Task RunBundleForBlazorWasmOrMauiBlazorTemplateAsync(ProjectBuildArgs projectArgs)
{
if (AppTemplateBase.IsAppTemplate(projectArgs.TemplateName) && projectArgs.UiFramework == UiFramework.Blazor)
if (AppTemplateBase.IsAppTemplate(projectArgs.TemplateName) && projectArgs.UiFramework is UiFramework.Blazor or UiFramework.MauiBlazor)
{
Logger.LogInformation("Generating bundles for Blazor Wasm...");
var isWebassembly = projectArgs.UiFramework == UiFramework.Blazor;
var message = isWebassembly ? "Generating bundles for Blazor Wasm" : "Generating bundles for MAUI Blazor";
Logger.LogInformation($"{message}...");
await EventBus.PublishAsync(new ProjectCreationProgressEvent
{
Message = "Generating bundles for Blazor Wasm"
Message = message
}, false);
var directory = Path.GetDirectoryName(
Directory.GetFiles(projectArgs.OutputFolder, "*.Blazor.csproj", SearchOption.AllDirectories).First()
Directory.GetFiles(projectArgs.OutputFolder, isWebassembly? "*.Blazor.csproj" :"*.MauiBlazor.csproj", SearchOption.AllDirectories).First()
);
await _bundlingService.BundleAsync(directory, true);
await _bundlingService.BundleAsync(directory, true, projectType: isWebassembly ? BundlingConsts.WebAssembly : BundlingConsts.MauiBlazor);
}
}
@ -531,7 +533,7 @@ public abstract class ProjectCreationCommandBase
}
}
protected virtual UiFramework GetUiFramework(CommandLineArgs commandLineArgs)
protected virtual UiFramework GetUiFramework(CommandLineArgs commandLineArgs, string template = "app")
{
if (commandLineArgs.Options.ContainsKey("no-ui"))
{
@ -554,6 +556,8 @@ public abstract class ProjectCreationCommandBase
return UiFramework.Blazor;
case "blazor-server":
return UiFramework.BlazorServer;
case "maui-blazor" when template == AppProTemplate.TemplateName:
return UiFramework.MauiBlazor;
default:
throw new CliUsageException(ExceptionMessageHelper.GetInvalidOptionExceptionMessage("UI Framework"));
}

@ -261,6 +261,12 @@ public class AbpIoSourceCodeStore : ISourceCodeStore, ITransientDependency
}
catch (Exception ex)
{
if(ex is UserFriendlyException)
{
Logger.LogWarning(ex.Message);
throw;
}
Console.WriteLine("Error occured while downloading source-code from {0} : {1}{2}{3}", url,
responseMessage?.ToString(), Environment.NewLine, ex.Message);
throw;

@ -7,5 +7,6 @@ public enum UiFramework
Mvc = 2,
Angular = 3,
Blazor = 4,
BlazorServer = 5
BlazorServer = 5,
MauiBlazor = 6
}

@ -132,7 +132,11 @@ public abstract class AppTemplateBase : TemplateInfo
case UiFramework.BlazorServer:
ConfigureWithBlazorServerUi(context, steps);
break;
case UiFramework.MauiBlazor:
ConfigureWithMauiBlazorUi(context, steps);
break;
case UiFramework.Mvc:
case UiFramework.NotSpecified:
ConfigureWithMvcUi(context, steps);
@ -150,6 +154,11 @@ public abstract class AppTemplateBase : TemplateInfo
steps.Add(new RemoveProjectFromSolutionStep("MyCompanyName.MyProjectName.Blazor.Server.Tiered"));
}
if (context.BuildArgs.UiFramework != UiFramework.MauiBlazor)
{
steps.Add(new RemoveProjectFromSolutionStep("MyCompanyName.MyProjectName.MauiBlazor"));
}
if (context.BuildArgs.UiFramework != UiFramework.Angular)
{
steps.Add(new RemoveFolderStep("/angular"));
@ -510,6 +519,37 @@ public abstract class AppTemplateBase : TemplateInfo
}
}
protected void ConfigureWithMauiBlazorUi(ProjectBuildContext context, List<ProjectBuildPipelineStep> steps)
{
context.Symbols.Add("ui:maui-blazor");
steps.Add(new MauiBlazorChangeApplicationIdGuidStep());
steps.Add(new RemoveProjectFromSolutionStep("MyCompanyName.MyProjectName.Web"));
steps.Add(new RemoveProjectFromSolutionStep("MyCompanyName.MyProjectName.Web.Host"));
steps.Add(new RemoveProjectFromSolutionStep("MyCompanyName.MyProjectName.Web.Tests", projectFolderPath: "/aspnet-core/test/MyCompanyName.MyProjectName.Web.Tests"));
if (context.BuildArgs.ExtraProperties.ContainsKey("separate-identity-server") ||
context.BuildArgs.ExtraProperties.ContainsKey("separate-auth-server"))
{
steps.Add(new RemoveProjectFromSolutionStep("MyCompanyName.MyProjectName.HttpApi.HostWithIds"));
steps.Add(new MauiBlazorPortChangeForSeparatedAuthServersStep());
steps.Add(new AppTemplateChangeDbMigratorPortSettingsStep("44300"));
if (context.BuildArgs.MobileApp == MobileApp.ReactNative)
{
steps.Add(new ReactEnvironmentFilePortChangeForSeparatedAuthServersStep());
}
}
else
{
steps.Add(new RemoveProjectFromSolutionStep("MyCompanyName.MyProjectName.HttpApi.Host"));
steps.Add(new RemoveProjectFromSolutionStep("MyCompanyName.MyProjectName.IdentityServer"));
steps.Add(new RemoveProjectFromSolutionStep("MyCompanyName.MyProjectName.AuthServer"));
steps.Add(new TemplateProjectRenameStep("MyCompanyName.MyProjectName.HttpApi.HostWithIds", "MyCompanyName.MyProjectName.HttpApi.Host"));
steps.Add(new AppTemplateChangeConsoleTestClientPortSettingsStep("44305"));
}
}
protected void RemoveUnnecessaryPorts(ProjectBuildContext context, List<ProjectBuildPipelineStep> steps)
{
steps.Add(new RemoveUnnecessaryPortsStep());
@ -599,7 +639,8 @@ public abstract class AppTemplateBase : TemplateInfo
{
if ((context.BuildArgs.UiFramework == UiFramework.Mvc
|| context.BuildArgs.UiFramework == UiFramework.Blazor
|| context.BuildArgs.UiFramework == UiFramework.BlazorServer) &&
|| context.BuildArgs.UiFramework == UiFramework.BlazorServer
|| context.BuildArgs.UiFramework == UiFramework.MauiBlazor) &&
context.BuildArgs.MobileApp == MobileApp.None)
{
steps.Add(new MoveFolderStep("/aspnet-core/", "/"));

@ -0,0 +1,34 @@
using System;
using System.Linq;
using System.Xml;
using Volo.Abp.Cli.ProjectBuilding.Building;
using Volo.Abp.Cli.Utils;
namespace Volo.Abp.Cli.ProjectBuilding.Templates.App;
public class MauiBlazorChangeApplicationIdGuidStep: ProjectBuildPipelineStep
{
public override void Execute(ProjectBuildContext context)
{
var projectFile = context.Files.FirstOrDefault(f => f.Name.EndsWith("MyCompanyName.MyProjectName.MauiBlazor.csproj"));
if (projectFile == null)
{
return;
}
using (var stream = StreamHelper.GenerateStreamFromString(projectFile.Content))
{
var doc = new XmlDocument { PreserveWhitespace = true };
doc.Load(stream);
var node = doc.SelectSingleNode("/Project/PropertyGroup/ApplicationIdGuid");
if (node != null)
{
node.InnerText = Guid.NewGuid().ToString();
}
projectFile.SetContent(doc.OuterXml);
}
}
}

@ -0,0 +1,50 @@
using System;
using System.Linq;
using Volo.Abp.Cli.ProjectBuilding.Building;
namespace Volo.Abp.Cli.ProjectBuilding.Templates.App;
public class MauiBlazorPortChangeForSeparatedAuthServersStep: ProjectBuildPipelineStep
{
public override void Execute(ProjectBuildContext context)
{
var appsettingsFile = context.Files.FirstOrDefault(x =>
!x.IsDirectory &&
x.Name.EndsWith("aspnet-core/src/MyCompanyName.MyProjectName.MauiBlazor/appsettings.json",
StringComparison.InvariantCultureIgnoreCase)
);
if(appsettingsFile == null)
{
return;
}
appsettingsFile.NormalizeLineEndings();
var lines = appsettingsFile.GetLines();
for (var i = 1; i < lines.Length; i++)
{
var line = lines[i];
var previousLine = lines[i-1];
if (line.Contains("Authority") && line.Contains("localhost"))
{
line = line.Replace("44305", "44301");
}
else if (previousLine.Contains("AbpAccountPublic") && line.Contains("BaseUrl") && line.Contains("localhost"))
{
line = line.Replace("44305", "44301");
}
else if (previousLine.Contains("Default") && line.Contains("BaseUrl") && line.Contains("localhost"))
{
line = line.Replace("44305", "44300");
}
lines[i] = line;
}
appsettingsFile.SetLines(lines);
}
}

@ -53,6 +53,7 @@ public class MauiChangePortStep : ProjectBuildPipelineStep
{
case UiFramework.Angular:
case UiFramework.Blazor:
case UiFramework.MauiBlazor:
authServerPort = "44305";
apiHostPort = "44305";
break;

@ -1,15 +1,23 @@
using System;
using Microsoft.Extensions.DependencyInjection;
namespace Volo.Abp.DependencyInjection;
/// <summary>
/// This class is equivalent of the <see cref="TransientCachedServiceProvider"/>.
/// Use <see cref="TransientCachedServiceProvider"/> instead of this class, for new projects.
/// </summary>
[ExposeServices(typeof(IAbpLazyServiceProvider))]
public class AbpLazyServiceProvider : CachedServiceProviderBase, IAbpLazyServiceProvider, ITransientDependency
public class AbpLazyServiceProvider :
CachedServiceProviderBase,
IAbpLazyServiceProvider,
ITransientDependency
{
public AbpLazyServiceProvider(IServiceProvider serviceProvider)
: base(serviceProvider)
{
}
public virtual T LazyGetRequiredService<T>()
{
return (T)LazyGetRequiredService(typeof(T));
@ -17,7 +25,7 @@ public class AbpLazyServiceProvider : CachedServiceProviderBase, IAbpLazyService
public virtual object LazyGetRequiredService(Type serviceType)
{
return GetService(serviceType);
return this.GetRequiredService(serviceType);
}
public virtual T LazyGetService<T>()
@ -32,24 +40,21 @@ public class AbpLazyServiceProvider : CachedServiceProviderBase, IAbpLazyService
public virtual T LazyGetService<T>(T defaultValue)
{
return (T)LazyGetService(typeof(T), defaultValue);
return GetService(defaultValue);
}
public virtual object LazyGetService(Type serviceType, object defaultValue)
{
return LazyGetService(serviceType) ?? defaultValue;
return GetService(serviceType, defaultValue);
}
public virtual T LazyGetService<T>(Func<IServiceProvider, object> factory)
{
return (T)LazyGetService(typeof(T), factory);
return GetService<T>(factory);
}
public virtual object LazyGetService(Type serviceType, Func<IServiceProvider, object> factory)
{
return CachedServices.GetOrAdd(
serviceType,
_ => new Lazy<object>(() => factory(ServiceProvider))
).Value;
return GetService(serviceType, factory);
}
}

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
namespace Volo.Abp.DependencyInjection;

@ -3,7 +3,7 @@ using System.Collections.Concurrent;
namespace Volo.Abp.DependencyInjection;
public abstract class CachedServiceProviderBase
public abstract class CachedServiceProviderBase : ICachedServiceProviderBase
{
protected IServiceProvider ServiceProvider { get; }
protected ConcurrentDictionary<Type, Lazy<object>> CachedServices { get; }
@ -22,4 +22,27 @@ public abstract class CachedServiceProviderBase
_ => new Lazy<object>(() => ServiceProvider.GetService(serviceType))
).Value;
}
public T GetService<T>(T defaultValue)
{
return (T)GetService(typeof(T), defaultValue);
}
public object GetService(Type serviceType, object defaultValue)
{
return GetService(serviceType) ?? defaultValue;
}
public T GetService<T>(Func<IServiceProvider, object> factory)
{
return (T)GetService(typeof(T), factory);
}
public object GetService(Type serviceType, Func<IServiceProvider, object> factory)
{
return CachedServices.GetOrAdd(
serviceType,
_ => new Lazy<object>(() => factory(ServiceProvider))
).Value;
}
}

@ -2,21 +2,57 @@
namespace Volo.Abp.DependencyInjection;
public interface IAbpLazyServiceProvider
/// <summary>
/// This service is equivalent of the <see cref="ITransientCachedServiceProvider"/>.
/// Use <see cref="ITransientCachedServiceProvider"/> instead of this interface, for new projects.
/// </summary>
public interface IAbpLazyServiceProvider : ICachedServiceProviderBase
{
/// <summary>
/// This method is equivalent of the GetRequiredService method.
/// It does exists for backward compatibility.
/// </summary>
T LazyGetRequiredService<T>();
/// <summary>
/// This method is equivalent of the GetRequiredService method.
/// It does exists for backward compatibility.
/// </summary>
object LazyGetRequiredService(Type serviceType);
/// <summary>
/// This method is equivalent of the GetService method.
/// It does exists for backward compatibility.
/// </summary>
T LazyGetService<T>();
/// <summary>
/// This method is equivalent of the GetService method.
/// It does exists for backward compatibility.
/// </summary>
object LazyGetService(Type serviceType);
/// <summary>
/// This method is equivalent of the <see cref="ICachedServiceProviderBase.GetService{T}(T)"/> method.
/// It does exists for backward compatibility.
/// </summary>
T LazyGetService<T>(T defaultValue);
/// <summary>
/// This method is equivalent of the <see cref="ICachedServiceProviderBase.GetService(Type, object)"/> method.
/// It does exists for backward compatibility.
/// </summary>
object LazyGetService(Type serviceType, object defaultValue);
/// <summary>
/// This method is equivalent of the <see cref="ICachedServiceProviderBase.GetService(Type, Func{IServiceProvider, object})"/> method.
/// It does exists for backward compatibility.
/// </summary>
object LazyGetService(Type serviceType, Func<IServiceProvider, object> factory);
/// <summary>
/// This method is equivalent of the <see cref="ICachedServiceProviderBase.GetService{T}(Func{IServiceProvider, object})"/> method.
/// It does exists for backward compatibility.
/// </summary>
T LazyGetService<T>(Func<IServiceProvider, object> factory);
}

@ -1,6 +1,4 @@
using System;
namespace Volo.Abp.DependencyInjection;
namespace Volo.Abp.DependencyInjection;
/// <summary>
/// Provides services by caching the resolved services.
@ -8,7 +6,7 @@ namespace Volo.Abp.DependencyInjection;
/// This service's lifetime is scoped and it should be used
/// for a limited scope.
/// </summary>
public interface ICachedServiceProvider : IServiceProvider
public interface ICachedServiceProvider : ICachedServiceProviderBase
{
}

@ -0,0 +1,14 @@
using System;
namespace Volo.Abp.DependencyInjection;
public interface ICachedServiceProviderBase : IServiceProvider
{
T GetService<T>(T defaultValue);
object GetService(Type serviceType, object defaultValue);
T GetService<T>(Func<IServiceProvider, object> factory);
object GetService(Type serviceType, Func<IServiceProvider, object> factory);
}

@ -1,5 +1,3 @@
using System;
namespace Volo.Abp.DependencyInjection;
/// <summary>
@ -8,7 +6,7 @@ namespace Volo.Abp.DependencyInjection;
/// This service's lifetime is transient.
/// <see cref="ICachedServiceProvider"/> for the one with scoped lifetime.
/// </summary>
public interface ITransientCachedServiceProvider : IServiceProvider
public interface ITransientCachedServiceProvider : ICachedServiceProviderBase
{
}

@ -2,7 +2,6 @@
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Volo.Abp.EventBus.Boxes;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.Timing;
using Volo.Abp.Uow;

@ -2,7 +2,7 @@
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Volo.Abp.EventBus.Boxes;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.Timing;
using Volo.Abp.Uow;

@ -2,7 +2,6 @@
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Volo.Abp.EventBus.Boxes;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.Timing;
using Volo.Abp.Uow;

@ -5,7 +5,6 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Volo.Abp.EventBus.Boxes;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.Timing;
using Volo.Abp.Uow;

@ -2,7 +2,7 @@
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Volo.Abp.EventBus.Boxes;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.Timing;
using Volo.Abp.Uow;

@ -5,7 +5,6 @@ using System.Threading.Tasks;
using Volo.Abp.BackgroundWorkers;
using Volo.Abp.DistributedLocking;
using Volo.Abp.EventBus.Abstractions;
using Volo.Abp.EventBus.Boxes;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.EventBus.Local;
using Volo.Abp.Guids;

@ -1,6 +1,4 @@
using Volo.Abp.EventBus.Distributed;
namespace Volo.Abp.EventBus.Boxes;
namespace Volo.Abp.EventBus.Distributed;
public static class AbpDistributedEventBusExtensions
{

@ -1,6 +1,6 @@
using System;
namespace Volo.Abp.EventBus.Boxes;
namespace Volo.Abp.EventBus.Distributed;
public class AbpEventBusBoxesOptions
{

@ -1,8 +1,7 @@
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.EventBus.Distributed;
namespace Volo.Abp.EventBus.Boxes;
namespace Volo.Abp.EventBus.Distributed;
public interface IInboxProcessor
{

@ -1,8 +1,7 @@
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.EventBus.Distributed;
namespace Volo.Abp.EventBus.Boxes;
namespace Volo.Abp.EventBus.Distributed;
public interface IOutboxSender
{

@ -5,9 +5,8 @@ using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.BackgroundWorkers;
using Volo.Abp.EventBus.Distributed;
namespace Volo.Abp.EventBus.Boxes;
namespace Volo.Abp.EventBus.Distributed;
public class InboxProcessManager : IBackgroundWorker
{

@ -7,12 +7,11 @@ using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DistributedLocking;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.Threading;
using Volo.Abp.Timing;
using Volo.Abp.Uow;
namespace Volo.Abp.EventBus.Boxes;
namespace Volo.Abp.EventBus.Distributed;
public class InboxProcessor : IInboxProcessor, ITransientDependency
{
@ -110,7 +109,7 @@ public class InboxProcessor : IInboxProcessor, ITransientDependency
await Inbox.MarkAsProcessedAsync(waitingEvent.Id);
await uow.CompleteAsync();
await uow.CompleteAsync(StoppingToken);
}
Logger.LogInformation($"Processed the incoming event with id = {waitingEvent.Id:N}");

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -10,10 +9,9 @@ using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DistributedLocking;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.Threading;
namespace Volo.Abp.EventBus.Boxes;
namespace Volo.Abp.EventBus.Distributed;
public class OutboxSender : IOutboxSender, ITransientDependency
{

@ -5,9 +5,8 @@ using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.BackgroundWorkers;
using Volo.Abp.EventBus.Distributed;
namespace Volo.Abp.EventBus.Boxes;
namespace Volo.Abp.EventBus.Distributed;
public class OutboxSenderManager : IBackgroundWorker
{

@ -6,7 +6,6 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using Volo.Abp.EventBus.Boxes;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.Timing;
using Volo.Abp.Uow;

@ -1,6 +1,9 @@
using Volo.Abp.AspNetCore.Components.Web.Theming;
using Localization.Resources.AbpUi;
using Volo.Abp.AspNetCore.Components.Web.Theming;
using Volo.Abp.FeatureManagement.Blazor.Settings;
using Volo.Abp.FeatureManagement.Localization;
using Volo.Abp.Features;
using Volo.Abp.Localization;
using Volo.Abp.Modularity;
using Volo.Abp.SettingManagement.Blazor;
@ -20,5 +23,12 @@ public class AbpFeatureManagementBlazorModule : AbpModule
{
options.Contributors.Add(new FeatureSettingManagementComponentContributor());
});
Configure<AbpLocalizationOptions>(options =>
{
options.Resources
.Get<AbpFeatureManagementResource>()
.AddBaseTypes(typeof(AbpUiResource));
});
}
}

@ -1,7 +1,10 @@
using Microsoft.Extensions.DependencyInjection;
using Localization.Resources.AbpUi;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AspNetCore.Components.Web.Theming.Routing;
using Volo.Abp.AutoMapper;
using Volo.Abp.BlazoriseUI;
using Volo.Abp.Identity.Localization;
using Volo.Abp.Localization;
using Volo.Abp.Modularity;
using Volo.Abp.ObjectExtending;
using Volo.Abp.ObjectExtending.Modularity;
@ -39,6 +42,15 @@ public class AbpIdentityBlazorModule : AbpModule
{
options.AdditionalAssemblies.Add(typeof(AbpIdentityBlazorModule).Assembly);
});
Configure<AbpLocalizationOptions>(options =>
{
options.Resources
.Get<IdentityResource>()
.AddBaseTypes(
typeof(AbpUiResource)
);
});
}
public override void PostConfigureServices(ServiceConfigurationContext context)

@ -14,22 +14,17 @@
@* ************************* PAGE HEADER ************************* *@
<PageHeader Title="@L["Users"]"
BreadcrumbItems="@BreadcrumbItems"
Toolbar="@Toolbar">
</PageHeader>
Toolbar="@Toolbar" />
</CardHeader>
<CardBody>
<Field Horizontal>
<FieldBody ColumnSize="ColumnSize.Is2.Is10.WithOffset">
<Addons>
<Addon AddonType="AddonType.Start">
<Label>@L["Search"]</Label>
</Addon>
<Addon AddonType="AddonType.Body">
<TextEdit Style="margin-left: 0.5rem" Size="Size.Small" Text="@GetListInput.Filter" TextChanged="@OnSearchTextChanged"/>
</Addon>
</Addons>
</FieldBody>
</Field>
<Row>
<Column ColumnSize="ColumnSize.Is3">
<Field>
<FieldLabel>@L["Search"]</FieldLabel>
<TextEdit Size="Size.Small" Text="@GetListInput.Filter" TextChanged="@OnSearchTextChanged" />
</Field>
</Column>
</Row>
@* ************************* DATA GRID ************************* *@
<AbpExtensibleDataGrid TItem="IdentityUserDto"
Data="Entities"

@ -61,8 +61,16 @@ public interface IIdentityUserRepository : IBasicRepository<IdentityUser, Guid>
string userName = null,
string phoneNumber = null,
string emailAddress = null,
string name = null,
string surname = null,
bool? isLockedOut = null,
bool? notActive = null,
bool? emailConfirmed = null,
bool? isExternal = null,
DateTime? maxCreationTime = null,
DateTime? minCreationTime = null,
DateTime? maxModifitionTime = null,
DateTime? minModifitionTime = null,
CancellationToken cancellationToken = default
);
@ -98,8 +106,16 @@ public interface IIdentityUserRepository : IBasicRepository<IdentityUser, Guid>
string userName = null,
string phoneNumber = null,
string emailAddress = null,
string name = null,
string surname = null,
bool? isLockedOut = null,
bool? notActive = null,
bool? emailConfirmed = null,
bool? isExternal = null,
DateTime? maxCreationTime = null,
DateTime? minCreationTime = null,
DateTime? maxModifitionTime = null,
DateTime? minModifitionTime = null,
CancellationToken cancellationToken = default
);

@ -142,8 +142,16 @@ public class EfCoreIdentityUserRepository : EfCoreRepository<IIdentityDbContext,
string userName = null,
string phoneNumber = null,
string emailAddress = null,
string name = null,
string surname = null,
bool? isLockedOut = null,
bool? notActive = null,
bool? emailConfirmed = null,
bool? isExternal = null,
DateTime? maxCreationTime = null,
DateTime? minCreationTime = null,
DateTime? maxModifitionTime = null,
DateTime? minModifitionTime = null,
CancellationToken cancellationToken = default)
{
return await (await GetDbSetAsync())
@ -162,8 +170,16 @@ public class EfCoreIdentityUserRepository : EfCoreRepository<IIdentityDbContext,
.WhereIf(!string.IsNullOrWhiteSpace(userName), x => x.UserName == userName)
.WhereIf(!string.IsNullOrWhiteSpace(phoneNumber), x => x.PhoneNumber == phoneNumber)
.WhereIf(!string.IsNullOrWhiteSpace(emailAddress), x => x.Email == emailAddress)
.WhereIf(isLockedOut == true, x => x.LockoutEnabled && x.LockoutEnd.Value.CompareTo(DateTime.UtcNow) > 0)
.WhereIf(notActive == true, x => !x.IsActive)
.WhereIf(!string.IsNullOrWhiteSpace(name), x => x.Name == name)
.WhereIf(!string.IsNullOrWhiteSpace(surname), x => x.Surname == surname)
.WhereIf(isLockedOut.HasValue, x => x.LockoutEnabled && x.LockoutEnd.Value.CompareTo(DateTime.UtcNow) > 0)
.WhereIf(notActive.HasValue, x => !x.IsActive)
.WhereIf(emailConfirmed.HasValue, x => x.EmailConfirmed)
.WhereIf(isExternal.HasValue, x => x.IsExternal)
.WhereIf(maxCreationTime != null, p => p.CreationTime <= maxCreationTime)
.WhereIf(minCreationTime != null, p => p.CreationTime >= minCreationTime)
.WhereIf(maxModifitionTime != null, p => p.LastModificationTime <= maxModifitionTime)
.WhereIf(minModifitionTime != null, p => p.LastModificationTime >= minModifitionTime)
.OrderBy(sorting.IsNullOrWhiteSpace() ? nameof(IdentityUser.UserName) : sorting)
.PageBy(skipCount, maxResultCount)
.ToListAsync(GetCancellationToken(cancellationToken));
@ -207,8 +223,16 @@ public class EfCoreIdentityUserRepository : EfCoreRepository<IIdentityDbContext,
string userName = null,
string phoneNumber = null,
string emailAddress = null,
string name = null,
string surname = null,
bool? isLockedOut = null,
bool? notActive = null,
bool? emailConfirmed = null,
bool? isExternal = null,
DateTime? maxCreationTime = null,
DateTime? minCreationTime = null,
DateTime? maxModifitionTime = null,
DateTime? minModifitionTime = null,
CancellationToken cancellationToken = default)
{
return await (await GetDbSetAsync())
@ -226,8 +250,16 @@ public class EfCoreIdentityUserRepository : EfCoreRepository<IIdentityDbContext,
.WhereIf(!string.IsNullOrWhiteSpace(userName), x => x.UserName == userName)
.WhereIf(!string.IsNullOrWhiteSpace(phoneNumber), x => x.PhoneNumber == phoneNumber)
.WhereIf(!string.IsNullOrWhiteSpace(emailAddress), x => x.Email == emailAddress)
.WhereIf(isLockedOut == true, x => x.LockoutEnabled && x.LockoutEnd.Value.CompareTo(DateTime.UtcNow) > 0)
.WhereIf(notActive == true, x => !x.IsActive)
.WhereIf(!string.IsNullOrWhiteSpace(name), x => x.Name == name)
.WhereIf(!string.IsNullOrWhiteSpace(surname), x => x.Surname == surname)
.WhereIf(isLockedOut.HasValue, x => x.LockoutEnabled && x.LockoutEnd.Value.CompareTo(DateTime.UtcNow) > 0)
.WhereIf(notActive.HasValue, x => !x.IsActive)
.WhereIf(emailConfirmed.HasValue, x => x.EmailConfirmed)
.WhereIf(isExternal.HasValue, x => x.IsExternal)
.WhereIf(maxCreationTime != null, p => p.CreationTime <= maxCreationTime)
.WhereIf(minCreationTime != null, p => p.CreationTime >= minCreationTime)
.WhereIf(maxModifitionTime != null, p => p.LastModificationTime <= maxModifitionTime)
.WhereIf(minModifitionTime != null, p => p.LastModificationTime >= minModifitionTime)
.LongCountAsync(GetCancellationToken(cancellationToken));
}
@ -305,11 +337,11 @@ public class EfCoreIdentityUserRepository : EfCoreRepository<IIdentityDbContext,
public virtual async Task<IdentityUser> FindByTenantIdAndUserNameAsync(
[NotNull] string userName,
Guid? tenantId,
Guid? tenantId,
bool includeDetails = true,
CancellationToken cancellationToken = default)
{
return await(await GetDbSetAsync())
return await (await GetDbSetAsync())
.IncludeDetails(includeDetails)
.FirstOrDefaultAsync(
u => u.TenantId == tenantId && u.UserName == userName,

@ -143,8 +143,16 @@ public class MongoIdentityUserRepository : MongoDbRepository<IAbpIdentityMongoDb
string userName = null,
string phoneNumber = null,
string emailAddress = null,
string name = null,
string surname = null,
bool? isLockedOut = null,
bool? notActive = null,
bool? emailConfirmed = null,
bool? isExternal = null,
DateTime? maxCreationTime = null,
DateTime? minCreationTime = null,
DateTime? maxModifitionTime = null,
DateTime? minModifitionTime = null,
CancellationToken cancellationToken = default)
{
return await (await GetMongoQueryableAsync(cancellationToken))
@ -162,8 +170,16 @@ public class MongoIdentityUserRepository : MongoDbRepository<IAbpIdentityMongoDb
.WhereIf<IdentityUser, IMongoQueryable<IdentityUser>>(!string.IsNullOrWhiteSpace(userName), x => x.UserName == userName)
.WhereIf<IdentityUser, IMongoQueryable<IdentityUser>>(!string.IsNullOrWhiteSpace(phoneNumber), x => x.PhoneNumber == phoneNumber)
.WhereIf<IdentityUser, IMongoQueryable<IdentityUser>>(!string.IsNullOrWhiteSpace(emailAddress), x => x.Email == emailAddress)
.WhereIf<IdentityUser, IMongoQueryable<IdentityUser>>(isLockedOut == true, x => x.LockoutEnabled && x.LockoutEnd > DateTimeOffset.UtcNow)
.WhereIf<IdentityUser, IMongoQueryable<IdentityUser>>(notActive == true, x => !x.IsActive)
.WhereIf<IdentityUser, IMongoQueryable<IdentityUser>>(!string.IsNullOrWhiteSpace(name), x => x.Name == name)
.WhereIf<IdentityUser, IMongoQueryable<IdentityUser>>(!string.IsNullOrWhiteSpace(surname), x => x.Surname == surname)
.WhereIf<IdentityUser, IMongoQueryable<IdentityUser>>(isLockedOut.HasValue, x => x.LockoutEnabled && x.LockoutEnd > DateTimeOffset.UtcNow)
.WhereIf<IdentityUser, IMongoQueryable<IdentityUser>>(notActive.HasValue, x => !x.IsActive)
.WhereIf<IdentityUser, IMongoQueryable<IdentityUser>>(emailConfirmed.HasValue, x => x.EmailConfirmed)
.WhereIf<IdentityUser, IMongoQueryable<IdentityUser>>(isExternal.HasValue, x => x.IsExternal)
.WhereIf(maxCreationTime != null, p => p.CreationTime <= maxCreationTime)
.WhereIf(minCreationTime != null, p => p.CreationTime >= minCreationTime)
.WhereIf(maxModifitionTime != null, p => p.LastModificationTime <= maxModifitionTime)
.WhereIf(minModifitionTime != null, p => p.LastModificationTime >= minModifitionTime)
.OrderBy(sorting.IsNullOrWhiteSpace() ? nameof(IdentityUser.UserName) : sorting)
.As<IMongoQueryable<IdentityUser>>()
.PageBy<IdentityUser, IMongoQueryable<IdentityUser>>(skipCount, maxResultCount)
@ -211,8 +227,16 @@ public class MongoIdentityUserRepository : MongoDbRepository<IAbpIdentityMongoDb
string userName = null,
string phoneNumber = null,
string emailAddress = null,
string name = null,
string surname = null,
bool? isLockedOut = null,
bool? notActive = null,
bool? emailConfirmed = null,
bool? isExternal = null,
DateTime? maxCreationTime = null,
DateTime? minCreationTime = null,
DateTime? maxModifitionTime = null,
DateTime? minModifitionTime = null,
CancellationToken cancellationToken = default)
{
return await (await GetMongoQueryableAsync(cancellationToken))
@ -230,8 +254,16 @@ public class MongoIdentityUserRepository : MongoDbRepository<IAbpIdentityMongoDb
.WhereIf<IdentityUser, IMongoQueryable<IdentityUser>>(!string.IsNullOrWhiteSpace(userName), x => x.UserName == userName)
.WhereIf<IdentityUser, IMongoQueryable<IdentityUser>>(!string.IsNullOrWhiteSpace(phoneNumber), x => x.PhoneNumber == phoneNumber)
.WhereIf<IdentityUser, IMongoQueryable<IdentityUser>>(!string.IsNullOrWhiteSpace(emailAddress), x => x.Email == emailAddress)
.WhereIf<IdentityUser, IMongoQueryable<IdentityUser>>(isLockedOut == true, x => x.LockoutEnabled && x.LockoutEnd > DateTimeOffset.UtcNow)
.WhereIf<IdentityUser, IMongoQueryable<IdentityUser>>(notActive == true, x => !x.IsActive)
.WhereIf<IdentityUser, IMongoQueryable<IdentityUser>>(!string.IsNullOrWhiteSpace(name), x => x.Name == name)
.WhereIf<IdentityUser, IMongoQueryable<IdentityUser>>(!string.IsNullOrWhiteSpace(surname), x => x.Surname == surname)
.WhereIf<IdentityUser, IMongoQueryable<IdentityUser>>(isLockedOut.HasValue, x => x.LockoutEnabled && x.LockoutEnd > DateTimeOffset.UtcNow)
.WhereIf<IdentityUser, IMongoQueryable<IdentityUser>>(notActive.HasValue, x => !x.IsActive)
.WhereIf<IdentityUser, IMongoQueryable<IdentityUser>>(emailConfirmed.HasValue, x => x.EmailConfirmed)
.WhereIf<IdentityUser, IMongoQueryable<IdentityUser>>(isExternal.HasValue, x => x.IsExternal)
.WhereIf<IdentityUser, IMongoQueryable<IdentityUser>>(maxCreationTime != null, p => p.CreationTime <= maxCreationTime)
.WhereIf<IdentityUser, IMongoQueryable<IdentityUser>>(minCreationTime != null, p => p.CreationTime >= minCreationTime)
.WhereIf<IdentityUser, IMongoQueryable<IdentityUser>>(maxModifitionTime != null, p => p.LastModificationTime <= maxModifitionTime)
.WhereIf<IdentityUser, IMongoQueryable<IdentityUser>>(minModifitionTime != null, p => p.LastModificationTime >= minModifitionTime)
.LongCountAsync(GetCancellationToken(cancellationToken));
}
@ -272,9 +304,9 @@ public class MongoIdentityUserRepository : MongoDbRepository<IAbpIdentityMongoDb
}
public virtual async Task<IdentityUser> FindByTenantIdAndUserNameAsync(
[NotNull] string userName,
Guid? tenantId,
bool includeDetails = true,
[NotNull] string userName,
Guid? tenantId,
bool includeDetails = true,
CancellationToken cancellationToken = default)
{
return await (await GetMongoQueryableAsync(cancellationToken))

@ -44,6 +44,7 @@ public class UserInfoController : AbpOpenIdDictControllerBase
claims[AbpClaimTypes.TenantId] = user.TenantId;
claims[OpenIddictConstants.Claims.PreferredUsername] = user.UserName;
claims[OpenIddictConstants.Claims.FamilyName] = user.Surname;
claims[OpenIddictConstants.Claims.GivenName] = user.Name;
}
if (User.HasScope(OpenIddictConstants.Scopes.Email))

@ -1,6 +1,9 @@
using Volo.Abp.AspNetCore.Components.Web.Theming;
using Localization.Resources.AbpUi;
using Volo.Abp.AspNetCore.Components.Web.Theming;
using Volo.Abp.AutoMapper;
using Volo.Abp.Localization;
using Volo.Abp.Modularity;
using Volo.Abp.PermissionManagement.Localization;
namespace Volo.Abp.PermissionManagement.Blazor;
@ -11,5 +14,15 @@ namespace Volo.Abp.PermissionManagement.Blazor;
)]
public class AbpPermissionManagementBlazorModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpLocalizationOptions>(options =>
{
options.Resources
.Get<AbpPermissionManagementResource>()
.AddBaseTypes(
typeof(AbpUiResource)
);
});
}
}

@ -1,10 +1,13 @@
using Microsoft.Extensions.DependencyInjection;
using Localization.Resources.AbpUi;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AspNetCore.Components.Web.Theming;
using Volo.Abp.AspNetCore.Components.Web.Theming.Routing;
using Volo.Abp.AutoMapper;
using Volo.Abp.Localization;
using Volo.Abp.Modularity;
using Volo.Abp.SettingManagement.Blazor.Menus;
using Volo.Abp.SettingManagement.Blazor.Settings;
using Volo.Abp.SettingManagement.Localization;
using Volo.Abp.UI.Navigation;
namespace Volo.Abp.SettingManagement.Blazor;
@ -39,5 +42,14 @@ public class AbpSettingManagementBlazorModule : AbpModule
{
options.Contributors.Add(new EmailingPageContributor());
});
Configure<AbpLocalizationOptions>(options =>
{
options.Resources
.Get<AbpSettingManagementResource>()
.AddBaseTypes(
typeof(AbpUiResource)
);
});
}
}

@ -1,11 +1,15 @@
using Microsoft.Extensions.DependencyInjection;
using Localization.Resources.AbpUi;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AspNetCore.Components.Web.Theming.Routing;
using Volo.Abp.AutoMapper;
using Volo.Abp.FeatureManagement.Blazor;
using Volo.Abp.FeatureManagement.Localization;
using Volo.Abp.Localization;
using Volo.Abp.Modularity;
using Volo.Abp.ObjectExtending;
using Volo.Abp.ObjectExtending.Modularity;
using Volo.Abp.TenantManagement.Blazor.Navigation;
using Volo.Abp.TenantManagement.Localization;
using Volo.Abp.Threading;
using Volo.Abp.UI.Navigation;
@ -38,6 +42,15 @@ public class AbpTenantManagementBlazorModule : AbpModule
{
options.AdditionalAssemblies.Add(typeof(AbpTenantManagementBlazorModule).Assembly);
});
Configure<AbpLocalizationOptions>(options =>
{
options.Resources
.Get<AbpTenantManagementResource>()
.AddBaseTypes(
typeof(AbpFeatureManagementResource),
typeof(AbpUiResource));
});
}
public override void PostConfigureServices(ServiceConfigurationContext context)

@ -36,7 +36,6 @@ export class PersonalSettingsComponent
form: UntypedFormGroup;
inProgress: boolean;
private profile: ProfileDto;
constructor(
private fb: UntypedFormBuilder,
@ -79,24 +78,12 @@ export class PersonalSettingsComponent
});
}
isDataSame(oldValue, newValue) {
return Object.entries(oldValue).some(([key, value]) => {
if (key in newValue) {
return value !== newValue[key];
}
return false;
});
}
logoutConfirmation = () => {
this.authService.logout().subscribe();
};
private isLogoutConfirmMessageActive() {
if (!this.isPersonalSettingsChangedConfirmationActive) {
return false;
}
return this.isDataSame(this.profile, this.form.value);
return this.isPersonalSettingsChangedConfirmationActive;
}
private showLogoutConfirmMessage() {

@ -47,6 +47,9 @@ $toastClass: abp-toast;
}
.#{$toastClass}-content {
position: relative;
display: flex;
align-self: center;
word-break: break-word;
.#{$toastClass}-close-button {
position: absolute;
top: 0;

@ -135,14 +135,20 @@ var abp = abp || {};
if (sourceName === '_') { //A convention to suppress the localization
return key;
}
if (sourceName) {
return abp.localization.internal.localize.apply(this, arguments).value;
}
sourceName = sourceName || abp.localization.defaultResourceName;
if (!sourceName) {
if (!abp.localization.defaultResourceName) {
abp.log.warn('Localization source name is not specified and the defaultResourceName was not defined!');
return key;
}
return abp.localization.internal.localize.apply(this, arguments).value;
var copiedArguments = Array.prototype.slice.call(arguments, 0);
copiedArguments.splice(1, 1, abp.localization.defaultResourceName);
return abp.localization.internal.localize.apply(this, copiedArguments).value;
};
abp.localization.isLocalized = function (key, sourceName) {

@ -70,13 +70,13 @@ public class MyProjectNameDbMigrationService : ITransientDependency
Logger.LogInformation("You can safely end this process...");
}
private async Task MigrateDatabaseSchemaAsync(Tenant tenant = null)
private async Task MigrateDatabaseSchemaAsync(Tenant? tenant = null)
{
Logger.LogInformation($"Migrating schema for {(tenant == null ? "host" : tenant.Name + " tenant")} database...");
await _dbSchemaMigrator.MigrateAsync();
}
private async Task SeedDataAsync(Tenant tenant = null)
private async Task SeedDataAsync(Tenant? tenant = null)
{
Logger.LogInformation($"Executing {(tenant == null ? "host" : tenant.Name + " tenant")} database seed...");

@ -2,13 +2,14 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blazorise.Bootstrap5" Version="1.1.3.1" />
<PackageReference Include="Blazorise.Icons.FontAwesome" Version="1.1.3.1" />
<PackageReference Include="Blazorise.Bootstrap5" Version="1.1.4.1" />
<PackageReference Include="Blazorise.Icons.FontAwesome" Version="1.1.4.1" />
<PackageReference Include="Serilog.AspNetCore" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
</ItemGroup>

@ -164,7 +164,7 @@ public class MyProjectNameModule : AbpModule
Configure<AppUrlOptions>(options =>
{
options.Applications["MVC"].RootUrl = configuration["App:SelfUrl"];
options.RedirectAllowedUrls.AddRange(configuration["App:RedirectAllowedUrls"].Split(','));
options.RedirectAllowedUrls.AddRange(configuration["App:RedirectAllowedUrls"]?.Split(',') ?? Array.Empty<string>());
});
}

@ -79,13 +79,13 @@ public class MyProjectNameDbMigrationService : ITransientDependency
Logger.LogInformation("You can safely end this process...");
}
private async Task MigrateDatabaseSchemaAsync(Tenant tenant = null)
private async Task MigrateDatabaseSchemaAsync(Tenant? tenant = null)
{
Logger.LogInformation($"Migrating schema for {(tenant == null ? "host" : tenant.Name + " tenant")} database...");
await _dbSchemaMigrator.MigrateAsync();
}
private async Task SeedDataAsync(Tenant tenant = null)
private async Task SeedDataAsync(Tenant? tenant = null)
{
Logger.LogInformation($"Executing {(tenant == null ? "host" : tenant.Name + " tenant")} database seed...");
@ -184,15 +184,15 @@ public class MyProjectNameDbMigrationService : ITransientDependency
return Path.Combine(slnDirectoryPath, "MyCompanyName.MyProjectName.Blazor.Server");
}
private string GetSolutionDirectoryPath()
private string? GetSolutionDirectoryPath()
{
var currentDirectory = new DirectoryInfo(Directory.GetCurrentDirectory());
while (Directory.GetParent(currentDirectory.FullName) != null)
while (currentDirectory != null && Directory.GetParent(currentDirectory.FullName) != null)
{
currentDirectory = Directory.GetParent(currentDirectory.FullName);
if (Directory.GetFiles(currentDirectory.FullName).FirstOrDefault(f => f.EndsWith(".sln")) != null)
if (currentDirectory != null && Directory.GetFiles(currentDirectory.FullName).FirstOrDefault(f => f.EndsWith(".sln")) != null)
{
return currentDirectory.FullName;
}

@ -2,13 +2,14 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blazorise.Bootstrap5" Version="1.1.3.1" />
<PackageReference Include="Blazorise.Icons.FontAwesome" Version="1.1.3.1" />
<PackageReference Include="Blazorise.Bootstrap5" Version="1.1.4.1" />
<PackageReference Include="Blazorise.Icons.FontAwesome" Version="1.1.4.1" />
<PackageReference Include="Serilog.AspNetCore" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
</ItemGroup>

@ -166,7 +166,7 @@ public class MyProjectNameModule : AbpModule
Configure<AppUrlOptions>(options =>
{
options.Applications["MVC"].RootUrl = configuration["App:SelfUrl"];
options.RedirectAllowedUrls.AddRange(configuration["App:RedirectAllowedUrls"].Split(','));
options.RedirectAllowedUrls.AddRange(configuration["App:RedirectAllowedUrls"]?.Split(',') ?? Array.Empty<string>());
});
}

@ -70,13 +70,13 @@ public class MyProjectNameDbMigrationService : ITransientDependency
Logger.LogInformation("You can safely end this process...");
}
private async Task MigrateDatabaseSchemaAsync(Tenant tenant = null)
private async Task MigrateDatabaseSchemaAsync(Tenant? tenant = null)
{
Logger.LogInformation($"Migrating schema for {(tenant == null ? "host" : tenant.Name + " tenant")} database...");
await _dbSchemaMigrator.MigrateAsync();
}
private async Task SeedDataAsync(Tenant tenant = null)
private async Task SeedDataAsync(Tenant? tenant = null)
{
Logger.LogInformation($"Executing {(tenant == null ? "host" : tenant.Name + " tenant")} database seed...");

@ -78,7 +78,7 @@ public class OpenIddictDataSeedContributor : IDataSeedContributor, ITransientDep
{
var webClientRootUrl = configurationSection["MyProjectName_App:RootUrl"]?.TrimEnd('/');
await CreateApplicationAsync(
name: consoleAndAngularClientId,
name: consoleAndAngularClientId!,
type: OpenIddictConstants.ClientTypes.Public,
consentType: OpenIddictConstants.ConsentTypes.Implicit,
displayName: "Console Test / Angular Application",
@ -100,10 +100,10 @@ public class OpenIddictDataSeedContributor : IDataSeedContributor, ITransientDep
var swaggerClientId = configurationSection["MyProjectName_Swagger:ClientId"];
if (!swaggerClientId.IsNullOrWhiteSpace())
{
var swaggerRootUrl = configurationSection["MyProjectName_Swagger:RootUrl"].TrimEnd('/');
var swaggerRootUrl = configurationSection["MyProjectName_Swagger:RootUrl"]?.TrimEnd('/');
await CreateApplicationAsync(
name: swaggerClientId,
name: swaggerClientId!,
type: OpenIddictConstants.ClientTypes.Public,
consentType: OpenIddictConstants.ConsentTypes.Implicit,
displayName: "Swagger Application",
@ -123,12 +123,12 @@ public class OpenIddictDataSeedContributor : IDataSeedContributor, ITransientDep
[NotNull] string type,
[NotNull] string consentType,
string displayName,
string secret,
string? secret,
List<string> grantTypes,
List<string> scopes,
string redirectUri = null,
string postLogoutRedirectUri = null,
List<string> permissions = null)
string? redirectUri = null,
string? postLogoutRedirectUri = null,
List<string>? permissions = null)
{
if (!string.IsNullOrEmpty(secret) && string.Equals(type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase))
{

@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>

@ -171,7 +171,7 @@ public class MyProjectNameModule : AbpModule
Configure<AppUrlOptions>(options =>
{
options.Applications["MVC"].RootUrl = configuration["App:SelfUrl"];
options.RedirectAllowedUrls.AddRange(configuration["App:RedirectAllowedUrls"].Split(','));
options.RedirectAllowedUrls.AddRange(configuration["App:RedirectAllowedUrls"]?.Split(',') ?? Array.Empty<string>());
options.Applications["Angular"].RootUrl = configuration["App:ClientUrl"];
options.Applications["Angular"].Urls[AccountUrlNames.PasswordReset] = "account/reset-password";
@ -275,10 +275,10 @@ public class MyProjectNameModule : AbpModule
{
builder
.WithOrigins(
configuration["App:CorsOrigins"]
configuration["App:CorsOrigins"]?
.Split(",", StringSplitOptions.RemoveEmptyEntries)
.Select(o => o.RemovePostFix("/"))
.ToArray()
.ToArray() ?? Array.Empty<string>()
)
.WithAbpExposedHeaders()
.SetIsOriginAllowedToAllowWildcardSubdomains()

@ -79,13 +79,13 @@ public class MyProjectNameDbMigrationService : ITransientDependency
Logger.LogInformation("You can safely end this process...");
}
private async Task MigrateDatabaseSchemaAsync(Tenant tenant = null)
private async Task MigrateDatabaseSchemaAsync(Tenant? tenant = null)
{
Logger.LogInformation($"Migrating schema for {(tenant == null ? "host" : tenant.Name + " tenant")} database...");
await _dbSchemaMigrator.MigrateAsync();
}
private async Task SeedDataAsync(Tenant tenant = null)
private async Task SeedDataAsync(Tenant? tenant = null)
{
Logger.LogInformation($"Executing {(tenant == null ? "host" : tenant.Name + " tenant")} database seed...");
@ -184,15 +184,15 @@ public class MyProjectNameDbMigrationService : ITransientDependency
return Path.Combine(slnDirectoryPath, "MyCompanyName.MyProjectName.Host");
}
private string GetSolutionDirectoryPath()
private string? GetSolutionDirectoryPath()
{
var currentDirectory = new DirectoryInfo(Directory.GetCurrentDirectory());
while (Directory.GetParent(currentDirectory.FullName) != null)
while (currentDirectory != null && Directory.GetParent(currentDirectory.FullName) != null)
{
currentDirectory = Directory.GetParent(currentDirectory.FullName);
if (Directory.GetFiles(currentDirectory.FullName).FirstOrDefault(f => f.EndsWith(".sln")) != null)
if (currentDirectory != null && Directory.GetFiles(currentDirectory.FullName).FirstOrDefault(f => f.EndsWith(".sln")) != null)
{
return currentDirectory.FullName;
}

@ -78,7 +78,7 @@ public class OpenIddictDataSeedContributor : IDataSeedContributor, ITransientDep
{
var webClientRootUrl = configurationSection["MyProjectName_App:RootUrl"]?.TrimEnd('/');
await CreateApplicationAsync(
name: consoleAndAngularClientId,
name: consoleAndAngularClientId!,
type: OpenIddictConstants.ClientTypes.Public,
consentType: OpenIddictConstants.ConsentTypes.Implicit,
displayName: "Console Test / Angular Application",
@ -100,10 +100,10 @@ public class OpenIddictDataSeedContributor : IDataSeedContributor, ITransientDep
var swaggerClientId = configurationSection["MyProjectName_Swagger:ClientId"];
if (!swaggerClientId.IsNullOrWhiteSpace())
{
var swaggerRootUrl = configurationSection["MyProjectName_Swagger:RootUrl"].TrimEnd('/');
var swaggerRootUrl = configurationSection["MyProjectName_Swagger:RootUrl"]?.TrimEnd('/');
await CreateApplicationAsync(
name: swaggerClientId,
name: swaggerClientId!,
type: OpenIddictConstants.ClientTypes.Public,
consentType: OpenIddictConstants.ConsentTypes.Implicit,
displayName: "Swagger Application",
@ -123,12 +123,12 @@ public class OpenIddictDataSeedContributor : IDataSeedContributor, ITransientDep
[NotNull] string type,
[NotNull] string consentType,
string displayName,
string secret,
string? secret,
List<string> grantTypes,
List<string> scopes,
string redirectUri = null,
string postLogoutRedirectUri = null,
List<string> permissions = null)
string? redirectUri = null,
string? postLogoutRedirectUri = null,
List<string>? permissions = null)
{
if (!string.IsNullOrEmpty(secret) && string.Equals(type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase))
{

@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>

@ -174,7 +174,7 @@ public class MyProjectNameModule : AbpModule
Configure<AppUrlOptions>(options =>
{
options.Applications["MVC"].RootUrl = configuration["App:SelfUrl"];
options.RedirectAllowedUrls.AddRange(configuration["App:RedirectAllowedUrls"].Split(','));
options.RedirectAllowedUrls.AddRange(configuration["App:RedirectAllowedUrls"]?.Split(',') ?? Array.Empty<string>());
options.Applications["Angular"].RootUrl = configuration["App:ClientUrl"];
options.Applications["Angular"].Urls[AccountUrlNames.PasswordReset] = "account/reset-password";
@ -278,10 +278,10 @@ public class MyProjectNameModule : AbpModule
{
builder
.WithOrigins(
configuration["App:CorsOrigins"]
configuration["App:CorsOrigins"]?
.Split(",", StringSplitOptions.RemoveEmptyEntries)
.Select(o => o.RemovePostFix("/"))
.ToArray()
.ToArray() ?? Array.Empty<string>()
)
.WithAbpExposedHeaders()
.SetIsOriginAllowedToAllowWildcardSubdomains()

@ -70,13 +70,13 @@ public class MyProjectNameDbMigrationService : ITransientDependency
Logger.LogInformation("You can safely end this process...");
}
private async Task MigrateDatabaseSchemaAsync(Tenant tenant = null)
private async Task MigrateDatabaseSchemaAsync(Tenant? tenant = null)
{
Logger.LogInformation($"Migrating schema for {(tenant == null ? "host" : tenant.Name + " tenant")} database...");
await _dbSchemaMigrator.MigrateAsync();
}
private async Task SeedDataAsync(Tenant tenant = null)
private async Task SeedDataAsync(Tenant? tenant = null)
{
Logger.LogInformation($"Executing {(tenant == null ? "host" : tenant.Name + " tenant")} database seed...");

@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>

@ -79,13 +79,13 @@ public class MyProjectNameDbMigrationService : ITransientDependency
Logger.LogInformation("You can safely end this process...");
}
private async Task MigrateDatabaseSchemaAsync(Tenant tenant = null)
private async Task MigrateDatabaseSchemaAsync(Tenant? tenant = null)
{
Logger.LogInformation($"Migrating schema for {(tenant == null ? "host" : tenant.Name + " tenant")} database...");
await _dbSchemaMigrator.MigrateAsync();
}
private async Task SeedDataAsync(Tenant tenant = null)
private async Task SeedDataAsync(Tenant? tenant = null)
{
Logger.LogInformation($"Executing {(tenant == null ? "host" : tenant.Name + " tenant")} database seed...");
@ -184,15 +184,15 @@ public class MyProjectNameDbMigrationService : ITransientDependency
return Path.Combine(slnDirectoryPath, "MyCompanyName.MyProjectName.Mvc");
}
private string GetSolutionDirectoryPath()
private string? GetSolutionDirectoryPath()
{
var currentDirectory = new DirectoryInfo(Directory.GetCurrentDirectory());
while (Directory.GetParent(currentDirectory.FullName) != null)
while (currentDirectory != null && Directory.GetParent(currentDirectory.FullName) != null)
{
currentDirectory = Directory.GetParent(currentDirectory.FullName);
if (Directory.GetFiles(currentDirectory.FullName).FirstOrDefault(f => f.EndsWith(".sln")) != null)
if (currentDirectory != null && Directory.GetFiles(currentDirectory.FullName).FirstOrDefault(f => f.EndsWith(".sln")) != null)
{
return currentDirectory.FullName;
}

@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>

@ -4,6 +4,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Nullable>enable</Nullable>
<RootNamespace>MyCompanyName.MyProjectName</RootNamespace>
</PropertyGroup>

@ -4,6 +4,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<RootNamespace>MyCompanyName.MyProjectName</RootNamespace>
</PropertyGroup>

@ -4,6 +4,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<RootNamespace>MyCompanyName.MyProjectName</RootNamespace>
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>

@ -114,7 +114,7 @@ public class MyProjectNameAuthServerModule : AbpModule
Configure<AppUrlOptions>(options =>
{
options.Applications["MVC"].RootUrl = configuration["App:SelfUrl"];
options.RedirectAllowedUrls.AddRange(configuration["App:RedirectAllowedUrls"].Split(','));
options.RedirectAllowedUrls.AddRange(configuration["App:RedirectAllowedUrls"]?.Split(',') ?? Array.Empty<string>());
options.Applications["Angular"].RootUrl = configuration["App:ClientUrl"];
options.Applications["Angular"].Urls[AccountUrlNames.PasswordReset] = "account/reset-password";
@ -150,10 +150,10 @@ public class MyProjectNameAuthServerModule : AbpModule
{
builder
.WithOrigins(
configuration["App:CorsOrigins"]
configuration["App:CorsOrigins"]?
.Split(",", StringSplitOptions.RemoveEmptyEntries)
.Select(o => o.RemovePostFix("/"))
.ToArray()
.ToArray() ?? Array.Empty<string>()
)
.WithAbpExposedHeaders()
.SetIsOriginAllowedToAllowWildcardSubdomains()

@ -19,10 +19,8 @@
<div class="d-flex align-items-center" style="min-height: 100vh;">
<div class="container">
<abp-row>
<div class="col mx-auto account-column">
<div class="account-brand p-4 text-center mb-1">
@if (!BrandingProvider.LogoUrl.IsNullOrEmpty())
{
<a class="navbar-brand" href="/" alt="@BrandingProvider.AppName"></a>
@ -33,9 +31,7 @@
}
</div>
<abp-card>
<abp-card-body>
<div class="container">
<abp-row>
<abp-column size="_9">
@ -68,52 +64,51 @@
<div class="ml-auto p-2 float-end">
<abp-dropdown>
<abp-dropdown-button text="@Model.CurrentLanguage" />
<abp-dropdown-menu>
@foreach (var language in Model.Languages)
{
<abp-dropdown-item href="~/Abp/Languages/Switch?culture=@(language.CultureName)&uiCulture=@(language.UiCultureName)&returnUrl=@(System.Net.WebUtility.UrlEncode(Request.GetEncodedPathAndQuery()))">@language.DisplayName</abp-dropdown-item>
}
</abp-dropdown-menu>
@if (@Model.Languages != null)
{
<abp-dropdown-menu>
@foreach (var language in Model.Languages)
{
<abp-dropdown-item href="~/Abp/Languages/Switch?culture=@(language.CultureName)&uiCulture=@(language.UiCultureName)&returnUrl=@(System.Net.WebUtility.UrlEncode(Request.GetEncodedPathAndQuery()))">@language.DisplayName</abp-dropdown-item>
}
</abp-dropdown-menu>
}
</abp-dropdown>
</div>
</abp-column>
</abp-row>
<hr class="m-4" />
<abp-row>
@foreach (var application in Model.Applications)
@if (Model.Applications != null)
{
<abp-column size-md="_4" class="mb-2">
<abp-card>
<abp-card-body>
@if (!application.LogoUri.IsNullOrEmpty())
{
<div class="mx-auto">
<img src="@application.LogoUri" style="height:64px" class="mb-3" />
foreach (var application in Model.Applications)
{
<abp-column size-md="@Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Grid.ColumnSize._4" class="mb-2">
<abp-card>
<abp-card-body>
@if (!application.LogoUri.IsNullOrEmpty())
{
<div class="mx-auto">
<img src="@application.LogoUri" style="height:64px" class="mb-3"/>
</div>
}
<h4>@application.DisplayName</h4>
<span class="text-muted">@application.ClientUri</span>
<div class="mt-1">
<a abp-button="@Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Button.AbpButtonType.Outline_Secondary" href="@application.ClientUri">@L["Visit"]</a>
</div>
}
<h4>@application.DisplayName</h4>
<span class="text-muted">@application.ClientUri</span>
<div class="mt-1">
<a abp-button="Outline_Secondary" href="@application.ClientUri">@L["Visit"]</a>
</div>
</abp-card-body>
</abp-card>
</abp-column>
</abp-card-body>
</abp-card>
</abp-column>
}
}
</abp-row>
</div>
</abp-card-body>
</abp-card>
</div>
</abp-row>
</div>
</div>
</div>

@ -9,11 +9,11 @@ namespace MyCompanyName.MyProjectName.Pages;
public class IndexModel : AbpPageModel
{
public List<OpenIddictApplication> Applications { get; protected set; }
public List<OpenIddictApplication>? Applications { get; protected set; }
public IReadOnlyList<LanguageInfo> Languages { get; protected set; }
public IReadOnlyList<LanguageInfo>? Languages { get; protected set; }
public string CurrentLanguage { get; protected set; }
public string? CurrentLanguage { get; protected set; }
protected IOpenIddictApplicationRepository OpenIdApplicationRepository { get; }

@ -4,6 +4,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
@ -13,8 +14,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blazorise.Bootstrap5" Version="1.1.3.1" />
<PackageReference Include="Blazorise.Icons.FontAwesome" Version="1.1.3.1" />
<PackageReference Include="Blazorise.Bootstrap5" Version="1.1.4.1" />
<PackageReference Include="Blazorise.Icons.FontAwesome" Version="1.1.4.1" />
<PackageReference Include="Serilog.AspNetCore" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="7.0.0" />

@ -4,6 +4,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
@ -13,8 +14,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blazorise.Bootstrap5" Version="1.1.3.1" />
<PackageReference Include="Blazorise.Icons.FontAwesome" Version="1.1.3.1" />
<PackageReference Include="Blazorise.Bootstrap5" Version="1.1.4.1" />
<PackageReference Include="Blazorise.Icons.FontAwesome" Version="1.1.4.1" />
<PackageReference Include="Serilog.AspNetCore" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
</ItemGroup>

@ -1,3 +1,4 @@
using System;
using System.IO;
using Blazorise.Bootstrap5;
using Blazorise.Icons.FontAwesome;
@ -111,7 +112,7 @@ public class MyProjectNameBlazorModule : AbpModule
Configure<AppUrlOptions>(options =>
{
options.Applications["MVC"].RootUrl = configuration["App:SelfUrl"];
options.RedirectAllowedUrls.AddRange(configuration["App:RedirectAllowedUrls"].Split(','));
options.RedirectAllowedUrls.AddRange(configuration["App:RedirectAllowedUrls"]?.Split(',') ?? Array.Empty<string>());
});
}

@ -4,6 +4,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
<!-- <TEMPLATE-REMOVE IF-NOT='PWA'> -->
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
@ -11,8 +12,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blazorise.Bootstrap5" Version="1.1.3.1" />
<PackageReference Include="Blazorise.Icons.FontAwesome" Version="1.1.3.1" />
<PackageReference Include="Blazorise.Bootstrap5" Version="1.1.4.1" />
<PackageReference Include="Blazorise.Icons.FontAwesome" Version="1.1.4.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.0" />
</ItemGroup>

@ -8,7 +8,7 @@
<base href="/" />
<!--ABP:Styles-->
<link href="global.css?_v=638042184565255290" rel="stylesheet"/>
<link href="global.css?_v=638052471030912513" rel="stylesheet"/>
<link href="main.css" rel="stylesheet"/>
<!--/ABP:Styles-->
<link href="MyCompanyName.MyProjectName.Blazor.styles.css" rel="stylesheet"/>
@ -29,7 +29,7 @@
</div>
<!--ABP:Scripts-->
<script src="global.js?_v=638042184568471262"></script>
<script src="global.js?_v=638052471033872019"></script>
<!--/ABP:Scripts-->
<!-- <TEMPLATE-REMOVE IF-NOT='PWA'> -->

@ -5,6 +5,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>

@ -4,6 +4,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Nullable>enable</Nullable>
<RootNamespace>MyCompanyName.MyProjectName</RootNamespace>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>

@ -87,7 +87,7 @@ public class MyProjectNameDbMigrationService : ITransientDependency
Logger.LogInformation("You can safely end this process...");
}
private async Task MigrateDatabaseSchemaAsync(Tenant tenant = null)
private async Task MigrateDatabaseSchemaAsync(Tenant? tenant = null)
{
Logger.LogInformation(
$"Migrating schema for {(tenant == null ? "host" : tenant.Name + " tenant")} database...");
@ -98,7 +98,7 @@ public class MyProjectNameDbMigrationService : ITransientDependency
}
}
private async Task SeedDataAsync(Tenant tenant = null)
private async Task SeedDataAsync(Tenant? tenant = null)
{
Logger.LogInformation($"Executing {(tenant == null ? "host" : tenant.Name + " tenant")} database seed...");
@ -152,8 +152,7 @@ public class MyProjectNameDbMigrationService : ITransientDependency
private bool MigrationsFolderExists()
{
var dbMigrationsProjectFolder = GetEntityFrameworkCoreProjectFolderPath();
return Directory.Exists(Path.Combine(dbMigrationsProjectFolder, "Migrations"));
return dbMigrationsProjectFolder != null && Directory.Exists(Path.Combine(dbMigrationsProjectFolder, "Migrations"));
}
private void AddInitialMigration()
@ -188,7 +187,7 @@ public class MyProjectNameDbMigrationService : ITransientDependency
}
}
private string GetEntityFrameworkCoreProjectFolderPath()
private string? GetEntityFrameworkCoreProjectFolderPath()
{
var slnDirectoryPath = GetSolutionDirectoryPath();
@ -203,15 +202,15 @@ public class MyProjectNameDbMigrationService : ITransientDependency
.FirstOrDefault(d => d.EndsWith(".EntityFrameworkCore"));
}
private string GetSolutionDirectoryPath()
private string? GetSolutionDirectoryPath()
{
var currentDirectory = new DirectoryInfo(Directory.GetCurrentDirectory());
while (Directory.GetParent(currentDirectory.FullName) != null)
while (currentDirectory != null && Directory.GetParent(currentDirectory.FullName) != null)
{
currentDirectory = Directory.GetParent(currentDirectory.FullName);
if (Directory.GetFiles(currentDirectory.FullName).FirstOrDefault(f => f.EndsWith(".sln")) != null)
if (currentDirectory != null && Directory.GetFiles(currentDirectory.FullName).FirstOrDefault(f => f.EndsWith(".sln")) != null)
{
return currentDirectory.FullName;
}

@ -4,6 +4,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<RootNamespace>MyCompanyName.MyProjectName</RootNamespace>
</PropertyGroup>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save