@ -0,0 +1,213 @@ |
|||||
|
# App Services vs Domain Services: Deep Dive into Two Core Service Types in ABP Framework |
||||
|
|
||||
|
In ABP's layered architecture, we frequently encounter two types of services that appear similar but serve distinctly different purposes: Application Services and Domain Services. Understanding the differences between them is crucial for building clear and maintainable enterprise applications. |
||||
|
|
||||
|
## Architectural Positioning |
||||
|
|
||||
|
In ABP's layered architecture: |
||||
|
|
||||
|
- **Application Services** reside in the application layer and are responsible for coordinating use case execution |
||||
|
- **Domain Services** reside in the domain layer and are responsible for implementing core business logic |
||||
|
|
||||
|
This layered design follows Domain-Driven Design (DDD) principles, ensuring clear separation of business logic and system maintainability. |
||||
|
|
||||
|
## Application Services: Use Case Orchestrators |
||||
|
|
||||
|
### Core Responsibilities |
||||
|
|
||||
|
Application Services are stateless services primarily used to implement application use cases. They act as a bridge between the presentation layer and domain layer, responsible for: |
||||
|
|
||||
|
- **Parameter Validation**: Input validation is automatically handled by ABP using data annotations |
||||
|
- **Authorization**: Checking user permissions and access control using `[Authorize]` attribute or manual authorization checks via `IAuthorizationService` |
||||
|
- **Transaction Management**: Methods automatically run as Unit of Work (transactional by default) |
||||
|
- **Use Case Orchestration**: Organizing and coordinating multiple domain objects to complete specific business use cases |
||||
|
- **Data Transformation**: Handling conversion between DTOs and domain objects using ObjectMapper |
||||
|
|
||||
|
### Design Principles |
||||
|
|
||||
|
1. **DTO Boundaries**: Application service methods should only accept and return DTOs, never directly expose domain entities |
||||
|
2. **Use Case Oriented**: Each method should correspond to a clear user use case |
||||
|
3. **Thin Layer Design**: Avoid implementing complex business logic in application services |
||||
|
|
||||
|
### Typical Execution Flow |
||||
|
|
||||
|
A standard application service method typically follows this pattern: |
||||
|
|
||||
|
```csharp |
||||
|
[Authorize(BookPermissions.Create)] // Declarative authorization |
||||
|
public virtual async Task<BookDto> CreateBookAsync(CreateBookDto input) // input is automatically validated |
||||
|
{ |
||||
|
// Get related data |
||||
|
var author = await _authorRepository.GetAsync(input.AuthorId); |
||||
|
|
||||
|
// Call domain service to execute business logic (if needed) |
||||
|
// You can also use the entity constructor directly if no complex business logic is required |
||||
|
var book = await _bookManager.CreateAsync(input.Title, author, input.Price); |
||||
|
|
||||
|
// Persist changes |
||||
|
await _bookRepository.InsertAsync(book); |
||||
|
|
||||
|
// Return DTO |
||||
|
return ObjectMapper.Map<Book, BookDto>(book); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Integration Services: Special kind of Application Service |
||||
|
|
||||
|
It's worth mentioning that ABP also provides a special type of application service—Integration Services. They are application services marked with the `[IntegrationService]` attribute, designed for inter-module or inter-microservice communication. |
||||
|
|
||||
|
We have a community article dedicated to integration services: [Integration Services Explained — What they are, when to use them, and how they behave](https://abp.io/community/articles/integration-services-explained-what-they-are-when-to-use-lienmsy8) |
||||
|
|
||||
|
## Domain Services: Guardians of Business Logic |
||||
|
|
||||
|
### Core Responsibilities |
||||
|
|
||||
|
Domain Services implement core business logic and are particularly needed when: |
||||
|
|
||||
|
- **Core domain logic depends on services**: You need to implement logic that requires repositories or other external services |
||||
|
- **Logic spans multiple aggregates**: The business logic is related to more than one aggregate/entity and doesn't properly fit in any single aggregate |
||||
|
- **Complex business rules**: Complex domain rules that don't naturally belong in a single entity |
||||
|
|
||||
|
### Design Principles |
||||
|
|
||||
|
1. **Domain Object Interaction**: Method parameters and return values should be domain objects (entities, value objects), never DTOs |
||||
|
2. **Business Logic Focus**: Focus on implementing pure business rules |
||||
|
3. **Stateless Design**: Maintain the stateless nature of services |
||||
|
4. **State-Changing Operations Only**: Domain services should only define methods that mutate data, not query methods |
||||
|
5. **No Authorization Logic**: Domain services should not perform authorization checks or depend on current user context |
||||
|
6. **Specific Method Names**: Use descriptive, business-meaningful method names (e.g., `AssignToAsync`) instead of generic names (e.g., `UpdateAsync`) |
||||
|
|
||||
|
### Implementation Example |
||||
|
|
||||
|
```csharp |
||||
|
public class IssueManager : DomainService |
||||
|
{ |
||||
|
private readonly IRepository<Issue, Guid> _issueRepository; |
||||
|
|
||||
|
public virtual async Task AssignToAsync(Issue issue, Guid userId) |
||||
|
{ |
||||
|
// Business rule: Check user's unfinished task count |
||||
|
var openIssueCount = await _issueRepository.GetCountAsync(i => i.AssignedUserId == userId && !i.IsClosed); |
||||
|
|
||||
|
if (openIssueCount >= 3) |
||||
|
{ |
||||
|
throw new BusinessException("IssueTracking:ConcurrentOpenIssueLimit"); |
||||
|
} |
||||
|
|
||||
|
// Execute assignment logic |
||||
|
issue.AssignedUserId = userId; |
||||
|
issue.AssignedDate = Clock.Now; |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Key Differences Comparison |
||||
|
|
||||
|
| Dimension | Application Services | Domain Services | |
||||
|
|-----------|---------------------|-----------------| |
||||
|
| **Layer Position** | Application Layer | Domain Layer | |
||||
|
| **Primary Responsibility** | Use Case Orchestration | Business Logic Implementation | |
||||
|
| **Data Interaction** | DTOs | Domain Objects | |
||||
|
| **Callers** | Presentation Layer/Client Applications | Application Services/Other Domain Services | |
||||
|
| **Authorization** | Responsible for permission checks | No authorization logic | |
||||
|
| **Transaction Management** | Manages transaction boundaries (Unit of Work) | Participates in transactions but doesn't manage | |
||||
|
| **Current User Context** | Can access current user information | Should not depend on current user context | |
||||
|
| **Return Types** | Returns DTOs | Returns domain objects only | |
||||
|
| **Query Operations** | Can perform query operations | Should not define GET/query methods | |
||||
|
| **Naming Convention** | `*AppService` | `*Manager` or `*Service` | |
||||
|
|
||||
|
## Collaboration Patterns in Practice |
||||
|
|
||||
|
In real-world development, these two types of services typically work together: |
||||
|
|
||||
|
```csharp |
||||
|
// Application Service |
||||
|
public class BookAppService : ApplicationService |
||||
|
{ |
||||
|
private readonly BookManager _bookManager; |
||||
|
private readonly IRepository<Book> _bookRepository; |
||||
|
|
||||
|
[Authorize(BookPermissions.Update)] |
||||
|
public virtual async Task<BookDto> UpdatePriceAsync(Guid id, decimal newPrice) |
||||
|
{ |
||||
|
var book = await _bookRepository.GetAsync(id); |
||||
|
|
||||
|
await _bookManager.ChangePriceAsync(book, newPrice); |
||||
|
|
||||
|
await _bookRepository.UpdateAsync(book); |
||||
|
|
||||
|
return ObjectMapper.Map<Book, BookDto>(book); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Domain Service |
||||
|
public class BookManager : DomainService |
||||
|
{ |
||||
|
public virtual async Task ChangePriceAsync(Book book, decimal newPrice) |
||||
|
{ |
||||
|
// Domain service focuses on business rules |
||||
|
if (newPrice <= 0) |
||||
|
{ |
||||
|
throw new BusinessException("Book:InvalidPrice"); |
||||
|
} |
||||
|
|
||||
|
if (book.IsDiscounted && newPrice > book.OriginalPrice) |
||||
|
{ |
||||
|
throw new BusinessException("Book:DiscountedPriceCannotExceedOriginal"); |
||||
|
} |
||||
|
|
||||
|
if (book.Price == newPrice) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Additional business logic: Check if price change requires approval |
||||
|
if (await RequiresApprovalAsync(book, newPrice)) |
||||
|
{ |
||||
|
throw new BusinessException("Book:PriceChangeRequiresApproval"); |
||||
|
} |
||||
|
|
||||
|
book.ChangePrice(newPrice); |
||||
|
} |
||||
|
|
||||
|
private Task<bool> RequiresApprovalAsync(Book book, decimal newPrice) |
||||
|
{ |
||||
|
// Example business rule: Large price increases require approval |
||||
|
var increasePercentage = ((newPrice - book.Price) / book.Price) * 100; |
||||
|
return Task.FromResult(increasePercentage > 50); // 50% increase threshold |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Best Practice Recommendations |
||||
|
|
||||
|
### Application Services |
||||
|
- Create a corresponding application service for each aggregate root |
||||
|
- Use clear naming conventions (e.g., `IBookAppService`) |
||||
|
- Implement standard CRUD operation methods (`GetAsync`, `CreateAsync`, `UpdateAsync`, `DeleteAsync`) |
||||
|
- Avoid inter-application service calls within the same module/application |
||||
|
- Always return DTOs, never expose domain entities directly |
||||
|
- Use the `[Authorize]` attribute for declarative authorization or manual checks via `IAuthorizationService` |
||||
|
- Methods automatically run as Unit of Work (transactional) |
||||
|
- Input validation is handled automatically by ABP |
||||
|
|
||||
|
### Domain Services |
||||
|
- Use the `Manager` suffix for naming (e.g., `BookManager`) |
||||
|
- Only define state-changing methods, avoid query methods (use repositories directly in Application Services for queries) |
||||
|
- Throw `BusinessException` with clear, unique error codes for domain validation failures |
||||
|
- Keep methods pure, avoid involving user context or authorization logic |
||||
|
- Accept and return domain objects only, never DTOs |
||||
|
- Use descriptive, business-meaningful method names (e.g., `AssignToAsync`, `ChangePriceAsync`) |
||||
|
- Do not implement interfaces unless there's a specific need for multiple implementations |
||||
|
|
||||
|
## Summary |
||||
|
|
||||
|
Application Services and Domain Services each have their distinct roles in the ABP framework: Application Services serve as use case orchestrators, handling authorization, validation, transaction management, and DTO transformations; Domain Services focus purely on business logic implementation without any infrastructure concerns. Integration Services are a special type of Application Service designed for inter-service communication. |
||||
|
|
||||
|
Correctly understanding and applying these service patterns is key to building high-quality ABP applications. Through clear separation of responsibilities, we can not only build more maintainable code but also flexibly switch between monolithic and microservice architectures—this is precisely the elegance of ABP framework design. |
||||
|
|
||||
|
## References |
||||
|
|
||||
|
- [Application Services](https://abp.io/docs/latest/framework/architecture/domain-driven-design/application-services) |
||||
|
- [Integration Services](https://abp.io/docs/latest/framework/api-development/integration-services) |
||||
|
- [Domain Services](https://abp.io/docs/latest/framework/architecture/domain-driven-design/domain-services) |
||||
|
After Width: | Height: | Size: 638 KiB |
@ -0,0 +1,338 @@ |
|||||
|
# Best Free Alternatives to AutoMapper in .NET — Why We Moved to Mapperly |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Introduction |
||||
|
|
||||
|
[AutoMapper](https://automapper.io/) has been one of the most popular mapping library for .NET apps. It has been free and [open-source](https://github.com/LuckyPennySoftware/AutoMapper) since 2009. On 16 April 2025, Jimmy Bogard (the owner of the project) decided to make it commercial for his own reasons. You can read [this announcement](https://www.jimmybogard.com/automapper-and-mediatr-licensing-update/) about what happened to AutoMapper. |
||||
|
|
||||
|
|
||||
|
|
||||
|
### Why AutoMapper’s licensing change matters |
||||
|
|
||||
|
In ABP Framework we have been also using AutoMapper for object mappings. After its commercial transition, we also needed to replace it. Because ABP Framework is open-source and under [LGPL-3.0 license](https://github.com/abpframework/abp#LGPL-3.0-1-ov-file). |
||||
|
|
||||
|
**TL;DR** |
||||
|
|
||||
|
> That's why, **we decided to replace AutoMapper with Mapperly**. |
||||
|
|
||||
|
In this article, we'll discuss the alternatives of AutoMapper so that you can cut down on costs and maximize performance while retaining control over your codebase. Also I'll explain why we chose Mapperly. |
||||
|
|
||||
|
Also AutoMapper uses heavily reflection. And reflection comes with a performance cost if used indiscriminately, and compile-time safety is limited. Let's see how we can overcome these... |
||||
|
|
||||
|
|
||||
|
|
||||
|
## Cost-Free Alternatives to AutoMapper |
||||
|
|
||||
|
Check out the comparison table for key features vs. AutoMapper. |
||||
|
|
||||
|
| | **AutoMapper (Paid)** | **Mapster (Free)** | **Mapperly (Free)** | **AgileMapper (Free)** | **Manual Mapping** | |
||||
|
| ------------------- | ----------------------------------------------- | ----------------------------------------- | -------------------------------------------- | ------------------------------------------- | ------------------------------------------------ | |
||||
|
| **License & Cost** | Paid/commercial | Free, MIT License | Free, MIT License | Free, Apache 2.0 | Free (no library) | |
||||
|
| **Performance** | Slower due to reflection & conventions | Very fast (runtime & compile-time modes) | Very fast (compile-time code generation) | Good, faster than AutoMapper | Fastest (direct assignment) | |
||||
|
| **Ease of Setup** | Easy, but configuration-heavy | Easy, minimal config | Easy, but different approach from AutoMapper | Simple, flexible configuration | Manual coding required | |
||||
|
| **Features** | Rich features, conventions, nested mappings | Strong typed mappings, projection support | Strong typed, compile-time safe mappings | Dynamic & conditional mapping | Whatever you code | |
||||
|
| **Maintainability** | Hidden mappings can be hard to debug | Explicit & predictable | Very explicit, compiler-verified mappings | Readable, good balance | Very explicit, most maintainable | |
||||
|
| **Best For** | Large teams used to AutoMapper & willing to pay | Teams wanting performance + free tool | Teams prioritizing type-safety & performance | Developers needing flexibility & simplicity | Small/medium projects, performance-critical apps | |
||||
|
|
||||
|
There are other libraries such as [**ExpressMapper**](https://github.com/fluentsprings/ExpressMapper) **(308 GitHub stars)**, [**ValueInjecter**](https://github.com/omuleanu/ValueInjecter) **(258 GitHub stars)**, [**AgileMapper**](https://github.com/agileobjects/AgileMapper) **(463 GitHub stars)**. These are not very popular but also free and offer a different balance of simplicity and features. |
||||
|
|
||||
|
|
||||
|
|
||||
|
## Why We Chose Mapperly |
||||
|
|
||||
|
We filtered down all the alternatives into 2: **Mapster** and **Mapperly**. |
||||
|
|
||||
|
The crucial factor was maintainability! As you see from the screenshots below, Mapster is already stopped development. Mapster’s development appears stalled, and its future maintenance is uncertain. On the other hand, Mapperly regularly gets commits. The community support is valuable. |
||||
|
|
||||
|
We looked up different alternatives of AutoMapper also, here's the initial issue of AutoMapper replacement [github.com/abpframework/abp/issues/23243](https://github.com/abpframework/abp/issues/23243). |
||||
|
|
||||
|
The ABP team started Mapperly integration with this initial commit [github.com/abpframework/abp/commit/178d3f56d42b4e5acb7e349470f4a644d4c5214e](https://github.com/abpframework/abp/commit/178d3f56d42b4e5acb7e349470f4a644d4c5214e). And this is our Mapperly integration package : [github.com/abpframework/abp/tree/dev/framework/src/Volo.Abp.Mapperly.](https://github.com/abpframework/abp/tree/dev/framework/src/Volo.Abp.Mapperly.) |
||||
|
|
||||
|
 |
||||
|
|
||||
|
Here are some considerations for developers who are used to ABP and AutoMapper. |
||||
|
|
||||
|
### [Mapster](https://github.com/MapsterMapper/Mapster): |
||||
|
|
||||
|
* ✔ It is similar to AutoMapper, configuring mappings through code. |
||||
|
* ✔ Support for dependency injection and complex runtime configuration. |
||||
|
* ❌ It is looking additional Mapster maintainers ([Call for additional Mapster maintainers MapsterMapper/Mapster#752](https://github.com/MapsterMapper/Mapster/discussions/752)) |
||||
|
|
||||
|
### [Mapperly](https://github.com/riok/Mapperly): |
||||
|
|
||||
|
- ✔ It generates mapping code(` source generator`) during the build process. |
||||
|
- ✔ It is actively being developed and maintained. |
||||
|
- ❌ It is a static `map` method, which is not friendly to dependency injection. |
||||
|
- ❌ The configuration method is completely different from AutoMapper, and there is a learning curve. |
||||
|
|
||||
|
|
||||
|
|
||||
|
**Mapperly** → generates mapping code at **compile time** using source generators. |
||||
|
|
||||
|
**Mapster** → has two modes: |
||||
|
|
||||
|
- By default, it uses **runtime code generation** (via expression trees and compilation). |
||||
|
|
||||
|
- But with **Mapster.Tool** (source generator), it can also generate mappings at **compile time**. |
||||
|
|
||||
|
|
||||
|
|
||||
|
This is important because it guarantees the mappings are working well. Also they provide type safety and improved performance. Another advantages of these libraries, they eliminate runtime surprises and offer better IDE support. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## When Mapperly Will Come To ABP |
||||
|
|
||||
|
Mapperly integration will be delivered with ABP v10. If you have already defined AutoMapper configurations, you can still keep and use them. But the framework will use Mapperly. So there'll be 2 mapping integrations in your app. You can also remove AutoMapper from your final application and use one mapping library: Mapperly. It's up to you! Check [AutoMapper pricing table](https://automapper.io/#pricing). |
||||
|
|
||||
|
|
||||
|
|
||||
|
## Migrating from AutoMapper to Mapperly |
||||
|
|
||||
|
In ABP v10, we will be migrating from AutoMapper to Mapperly. The document about the migration is not delivered by the time I wrote this article, but you can reach the document in our dev docs branch |
||||
|
|
||||
|
* [github.com/abpframework/abp/blob/dev/docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md](https://github.com/abpframework/abp/blob/dev/docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md). |
||||
|
|
||||
|
Also for ABP, you can check out how you will define DTO mappings based on Mapperly at this document |
||||
|
|
||||
|
* [github.com/abpframework/abp/blob/dev/docs/en/framework/infrastructure/object-to-object-mapping.md](https://github.com/abpframework/abp/blob/dev/docs/en/framework/infrastructure/object-to-object-mapping.md) |
||||
|
|
||||
|
|
||||
|
|
||||
|
## Mapping Code Examples for AutoMapper, Mapster, AgileMapper |
||||
|
|
||||
|
### AutoMapper vs Mapster vs Mapperly Performance |
||||
|
|
||||
|
Here are concise, drop-in **side-by-side C# snippets** that map the same model with AutoMapper, Mapster, AgileMapper, and manual mapping. |
||||
|
|
||||
|
Models used in all examples |
||||
|
|
||||
|
We'll use these models to show the mapping examples for AutoMapper, Mapster, AgileMapper. |
||||
|
|
||||
|
```csharp |
||||
|
public class Order |
||||
|
{ |
||||
|
public int Id { get; set; } |
||||
|
public Customer Customer { get; set; } = default!; |
||||
|
public List<OrderLine> Lines { get; set; } = new(); |
||||
|
public DateTime CreatedAt { get; set; } |
||||
|
} |
||||
|
|
||||
|
public class Customer |
||||
|
{ |
||||
|
public int Id { get; set; } |
||||
|
public string Name { get; set; } = ""; |
||||
|
public string? Email { get; set; } |
||||
|
} |
||||
|
|
||||
|
public class OrderLine |
||||
|
{ |
||||
|
public int ProductId { get; set; } |
||||
|
public int Quantity { get; set; } |
||||
|
public decimal UnitPrice { get; set; } |
||||
|
} |
||||
|
|
||||
|
public class OrderDto |
||||
|
{ |
||||
|
public int Id { get; set; } |
||||
|
public string CustomerName { get; set; } = ""; |
||||
|
public int ItemCount { get; set; } |
||||
|
public decimal Total { get; set; } |
||||
|
public string CreatedAtIso { get; set; } = ""; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
|
||||
|
#### AutoMapper Example (Paid) |
||||
|
|
||||
|
```csharp |
||||
|
public sealed class OrderProfile : Profile |
||||
|
{ |
||||
|
public OrderProfile() |
||||
|
{ |
||||
|
CreateMap<Order, OrderDto>() |
||||
|
.ForMember(d => d.CustomerName, m => m.MapFrom(s => s.Customer.Name)) |
||||
|
.ForMember(d => d.ItemCount, m => m.MapFrom(s => s.Lines.Sum(l => l.Quantity))) |
||||
|
.ForMember(d => d.Total, m => m.MapFrom(s => s.Lines.Sum(l => l.Quantity * l.UnitPrice))) |
||||
|
.ForMember(d => d.CreatedAtIso,m => m.MapFrom(s => s.CreatedAt.ToString("O"))); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// registration |
||||
|
services.AddAutoMapper(typeof(OrderProfile)); |
||||
|
|
||||
|
// mapping |
||||
|
var dto = mapper.Map<OrderDto>(order); |
||||
|
|
||||
|
// EF Core projection (common pattern) |
||||
|
var list = dbContext.Orders |
||||
|
.ProjectTo<OrderDto>(mapper.ConfigurationProvider) |
||||
|
.ToList(); |
||||
|
``` |
||||
|
|
||||
|
**NuGet Packages:** |
||||
|
|
||||
|
- https://www.nuget.org/packages/AutoMapper |
||||
|
- https://www.nuget.org/packages/AutoMapper.Extensions.Microsoft.DependencyInjection |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
#### Mapperly (Free, Apache-2.0) |
||||
|
|
||||
|
This is compile-time generated mapping. |
||||
|
|
||||
|
```csharp |
||||
|
[Mapper] // generates the implementation at build time |
||||
|
public partial class OrderMapper |
||||
|
{ |
||||
|
// Simple property mapping: Customer.Name -> CustomerName |
||||
|
[MapProperty(nameof(Order.Customer) + "." + nameof(Customer.Name), nameof(OrderDto.CustomerName))] |
||||
|
public partial OrderDto ToDto(Order source); |
||||
|
|
||||
|
// Update an existing target (like MapToExisting) |
||||
|
[MapProperty(nameof(Order.Customer) + "." + nameof(Customer.Name), nameof(OrderDto.CustomerName))] |
||||
|
public partial void UpdateDto(Order source, OrderDto target); |
||||
|
|
||||
|
public OrderDto Map(Order s) |
||||
|
{ |
||||
|
var d = ToDto(s); |
||||
|
AfterMap(s, d); |
||||
|
return d; |
||||
|
} |
||||
|
|
||||
|
public void Map(Order source, OrderDto d) |
||||
|
{ |
||||
|
UpdateDto(source, d); |
||||
|
AfterMap(source, d); |
||||
|
} |
||||
|
|
||||
|
private void AfterMap(Order source, OrderDto d) |
||||
|
{ |
||||
|
d.ItemCount = source.Lines.Sum(l => l.Quantity); |
||||
|
d.Total = source.Lines.Sum(l => l.Quantity * l.UnitPrice); |
||||
|
d.CreatedAtIso = source.CreatedAt.ToString("O"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//USAGE |
||||
|
var mapper = new OrderMapper(); |
||||
|
var order = new Order |
||||
|
{ |
||||
|
Id = 1, |
||||
|
Customer = new Customer { Id = 1, Name = "John Doe", Email = "johndoe@abp.io" }, |
||||
|
Lines = |
||||
|
[ |
||||
|
new OrderLine {ProductId = 1, Quantity = 2, UnitPrice = 10.0m}, |
||||
|
new OrderLine {ProductId = 2, Quantity = 1, UnitPrice = 20.0m} |
||||
|
] |
||||
|
}; |
||||
|
|
||||
|
// Map to a new object |
||||
|
var dto = mapper.Map(order); |
||||
|
|
||||
|
// Map to an existing object |
||||
|
var target = new OrderDto(); |
||||
|
mapper.Map(order, target); |
||||
|
``` |
||||
|
|
||||
|
**NuGet Packages:** |
||||
|
|
||||
|
* https://www.nuget.org/packages/Riok.Mapperly/ |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
#### Mapster Example (Free, MIT) |
||||
|
|
||||
|
```csharp |
||||
|
TypeAdapterConfig<Order, OrderDto>.NewConfig() |
||||
|
.Map(d => d.CustomerName, s => s.Customer.Name) |
||||
|
.Map(d => d.ItemCount, s => s.Lines.Sum(l => l.Quantity)) |
||||
|
.Map(d => d.Total, s => s.Lines.Sum(l => l.Quantity * l.UnitPrice)) |
||||
|
.Map(d => d.CreatedAtIso, s => s.CreatedAt.ToString("O")); |
||||
|
|
||||
|
// one-off |
||||
|
var dto = order.Adapt<OrderDto>(); |
||||
|
|
||||
|
// DI-friendly registration |
||||
|
services.AddSingleton(TypeAdapterConfig.GlobalSettings); |
||||
|
services.AddScoped<IMapper, ServiceMapper>(); |
||||
|
|
||||
|
// EF Core projection (strong suit) |
||||
|
var mappedList = dbContext.Orders |
||||
|
.ProjectToType<OrderDto>() // Mapster projection |
||||
|
.ToList(); |
||||
|
``` |
||||
|
|
||||
|
**NuGet Packages:** |
||||
|
|
||||
|
- https://www.nuget.org/packages/Mapster |
||||
|
- https://www.nuget.org/packages/Mapster.DependencyInjection |
||||
|
- https://www.nuget.org/packages/Mapster.SourceGenerator (for performance improvement) |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
#### AgileMapper Example (Free, Apache-2.0) |
||||
|
|
||||
|
```csharp |
||||
|
var mapper = Mapper.CreateNew(cfg => |
||||
|
{ |
||||
|
cfg.WhenMapping |
||||
|
.From<Order>() |
||||
|
.To<OrderDto>() |
||||
|
.Map(ctx => ctx.Source.Customer.Name).To(dto => dto.CustomerName) |
||||
|
.Map(ctx => ctx.Source.Lines.Sum(l => l.Quantity)).To(dto => dto.ItemCount) |
||||
|
.Map(ctx => ctx.Source.Lines.Sum(l => l.Quantity * l.UnitPrice)).To(dto => dto.Total) |
||||
|
.Map(ctx => ctx.Source.CreatedAt.ToString("O")).To(dto => dto.CreatedAtIso); |
||||
|
}); |
||||
|
|
||||
|
var mappedDto = mapper.Map(order).ToANew<OrderDto>(); |
||||
|
``` |
||||
|
|
||||
|
**NuGet Packages:** |
||||
|
|
||||
|
* https://www.nuget.org/packages/AgileObjects.AgileMapper |
||||
|
|
||||
|
|
||||
|
--- |
||||
|
|
||||
|
#### Manual (Pure) Mapping (no library) |
||||
|
|
||||
|
Straightforward, fastest, and most explicit. Good for simple applications which doesn't need long term maintenance. Hand-written mapping is faster, safer, and more maintainable. And for tiny mappings, you can still use manual mapping. |
||||
|
|
||||
|
* Examples of when manual mapping is better than libraries. |
||||
|
|
||||
|
```csharp |
||||
|
public static class OrderMapping |
||||
|
{ |
||||
|
public static OrderDto ToDto(this Order s) => new() |
||||
|
{ |
||||
|
Id = s.Id, |
||||
|
CustomerName = s.Customer.Name, |
||||
|
ItemCount = s.Lines.Sum(l => l.Quantity), |
||||
|
Total = s.Lines.Sum(l => l.Quantity * l.UnitPrice), |
||||
|
CreatedAtIso = s.CreatedAt.ToString("O") |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
// usage |
||||
|
var dto = order.ToDto(); |
||||
|
|
||||
|
// EF Core projection (best for perf + SQL translation) |
||||
|
var mappedList = dbContext.Orders.Select(s => new OrderDto |
||||
|
{ |
||||
|
Id = s.Id, |
||||
|
CustomerName = s.Customer.Name, |
||||
|
ItemCount = s.Lines.Sum(l => l.Quantity), |
||||
|
Total = s.Lines.Sum(l => l.Quantity * l.UnitPrice), |
||||
|
CreatedAtIso = s.CreatedAt.ToString("O") |
||||
|
}).ToList(); |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
|
||||
|
### Conclusion |
||||
|
|
||||
|
If you rely on AutoMapper today, it’s time to evaluate alternatives. For ABP Framework, we chose **Mapperly** due to active development, strong community, and compile-time performance. But your team may prefer **Mapster** for flexibility or even manual mapping for small apps. Your requirements might be different, your project is not a framework so you decide the best one for you. |
||||
|
After Width: | Height: | Size: 477 KiB |
|
After Width: | Height: | Size: 163 KiB |
@ -0,0 +1,174 @@ |
|||||
|
# Building a Permission-Based Authorization System for ASP.NET Core |
||||
|
|
||||
|
In this article, we'll explore different authorization approaches in ASP.NET Core and examine how ABP's permission-based authorization system works. |
||||
|
|
||||
|
First, we'll look at some of the core authorization types that come with ASP.NET Core, such as role-based, claims-based, policy-based, and resource-based authorization. We'll briefly review the pros and cons of each approach. |
||||
|
|
||||
|
Then, we'll dive into [ABP's Permission-Based Authorization System](https://abp.io/docs/latest/framework/fundamentals/authorization#permission-system). This is a more advanced approach that gives you fine-grained control over what users can do in your application. We'll also explore ABP's Permission Management Module, which makes managing permissions through the UI easily. |
||||
|
|
||||
|
## Understanding ASP.NET Core Authorization Types |
||||
|
|
||||
|
Before diving into permission-based authorization, let's examine some of the core authorization types available in ASP.NET Core: |
||||
|
|
||||
|
- **[Role-Based Authorization](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/roles?view=aspnetcore-9.0)** checks if the current user belongs to specific roles (like **"Admin"** or **"User"**) and grants access based on these roles. (For example, only users in the **"Manager"** role can access the employee salary management page.) |
||||
|
|
||||
|
- **[Claims-Based Authorization](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/claims?view=aspnetcore-9.0)** uses key-value pairs (claims) that describe user attributes, such as age, department, or security clearance. (For example, only users with a **"Department=Finance"** claim can view financial reports.) This provides more granular control but requires careful claim management (such as grouping claims under policies). |
||||
|
|
||||
|
- **[Policy-Based Authorization](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-9.0)** combines multiple requirements (roles, claims, custom logic) into reusable policies. It offers flexibility and centralized management, and **this is exactly why ABP's permission system is built on top of it!** (We'll discuss this in more detail later.) |
||||
|
|
||||
|
- **[Resource-Based Authorization](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/resourcebased?view=aspnetcore-9.0)** determines access by examining both the user and the specific item they want to access. (For example, a user can edit only their own blog posts, not others' posts.) Unlike policy-based authorization which applies the same rules everywhere, resource-based authorization makes decisions based on the actual data being accessed, requiring more complex implementation. |
||||
|
|
||||
|
Here's a quick comparison of these approaches: |
||||
|
|
||||
|
| Authorization Type | Pros | Cons | |
||||
|
|-------------------|------|------| |
||||
|
| **Role-Based** | Simple implementation, easy to understand | Becomes inflexible with complex role hierarchies | |
||||
|
| **Claims-Based** | Granular control, flexible user attributes | Complex claim management, potential for claim explosion | |
||||
|
| **Policy-Based** | Centralized logic, combines multiple requirements | Can become complex with numerous policies | |
||||
|
| **Resource-Based** | Fine-grained per-resource control | Implementation complexity, resource-specific code | |
||||
|
|
||||
|
## What is Permission-Based Authorization? |
||||
|
|
||||
|
Permission-based authorization takes a different approach from other authorization types by defining specific permissions (like **"CreateUser"**, **"DeleteOrder"**, **"ViewReports"**) that represent granular actions within your application. These permissions can be assigned to users directly or through roles, providing both flexibility and clear action-based access control. |
||||
|
|
||||
|
ABP Framework's permission system is built on top of this approach and extends ASP.NET Core's policy-based authorization system, working seamlessly with it. |
||||
|
|
||||
|
## ABP Framework's Permission System |
||||
|
|
||||
|
ABP extends [ASP.NET Core Authorization](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/introduction?view=aspnetcore-9.0) by adding **permissions** as automatic [policies](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-9.0) and allows the authorization system to be used in application services as well. |
||||
|
|
||||
|
This system provides a clean abstraction while maintaining full compatibility with ASP.NET Core's authorization infrastructure. |
||||
|
|
||||
|
ABP also provides a [Permission Management Module](https://abp.io/docs/latest/modules/permission-management) that offers a complete UI and API for managing permissions. This allows you to easily manage permissions in the UI, assign permissions to roles or users, and much more. (We'll see how to use it in the following sections.) |
||||
|
|
||||
|
### Defining Permissions in ABP |
||||
|
|
||||
|
In ABP, permissions are defined in classes (typically under the `*.Application.Contracts` project) that inherit from the `PermissionDefinitionProvider` class. Here's how you can define permissions for a book management system: |
||||
|
|
||||
|
```csharp |
||||
|
public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvider |
||||
|
{ |
||||
|
public override void Define(IPermissionDefinitionContext context) |
||||
|
{ |
||||
|
var bookStoreGroup = context.AddGroup("BookStore"); |
||||
|
|
||||
|
var booksPermission = bookStoreGroup.AddPermission("BookStore.Books", L("Permission:Books")); |
||||
|
booksPermission.AddChild("BookStore.Books.Create", L("Permission:Books.Create")); |
||||
|
booksPermission.AddChild("BookStore.Books.Edit", L("Permission:Books.Edit")); |
||||
|
booksPermission.AddChild("BookStore.Books.Delete", L("Permission:Books.Delete")); |
||||
|
} |
||||
|
|
||||
|
private static LocalizableString L(string name) |
||||
|
{ |
||||
|
return LocalizableString.Create<BookStoreResource>(name); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
ABP automatically discovers this class and registers the permissions/policies in the system. You can then assign these permissions/policies to users/roles. There are two ways to do this: |
||||
|
|
||||
|
* Using the [Permission Management Module](https://abp.io/docs/latest/modules/permission-management) |
||||
|
* Using the `IPermissionManager` service (via code) |
||||
|
|
||||
|
#### Setting Permissions to Roles and Users via Permission Management Module |
||||
|
|
||||
|
When you define a permission, it also becomes usable in the ASP.NET Core authorization system as a **policy name**. If you are using the [Permission Management Module](https://abp.io/docs/latest/modules/permission-management), you can manage the permissions through the UI: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
In the permission management UI, you can grant permissions to roles and users through the **Role Management** and **User Management** pages within the "permissions" modals. You can then easily check these permissions in your code. In the screenshot above, you can see the permission modal for the user's page, clearly showing the permissions granted to the user by their role. (**(R)** in the UI indicates that the permission is granted by one of the current user's roles.) |
||||
|
|
||||
|
#### Setting Permissions to Roles and Users via Code |
||||
|
|
||||
|
You can also set permissions for roles and users programmatically. You just need to inject the `IPermissionManager` service and use its `SetForRoleAsync` and `SetForUserAsync` methods (or similar methods): |
||||
|
|
||||
|
```csharp |
||||
|
public class MyService : ITransientDependency |
||||
|
{ |
||||
|
private readonly IPermissionManager _permissionManager; |
||||
|
|
||||
|
public MyService(IPermissionManager permissionManager) |
||||
|
{ |
||||
|
_permissionManager = permissionManager; |
||||
|
} |
||||
|
|
||||
|
public async Task GrantPermissionForUserAsync(Guid userId, string permissionName) |
||||
|
{ |
||||
|
await _permissionManager.SetForUserAsync(userId, permissionName, true); |
||||
|
} |
||||
|
|
||||
|
public async Task ProhibitPermissionForUserAsync(Guid userId, string permissionName) |
||||
|
{ |
||||
|
await _permissionManager.SetForUserAsync(userId, permissionName, false); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Checking Permissions in AppServices and Controllers |
||||
|
|
||||
|
ABP provides multiple ways to check permissions. The most common approach is using the `[Authorize]` attribute and passing the permission/policy name. |
||||
|
|
||||
|
Here is an example of how to check permissions in an application service: |
||||
|
|
||||
|
```csharp |
||||
|
[Authorize("BookStore.Books")] |
||||
|
public class BookAppService : ApplicationService, IBookAppService |
||||
|
{ |
||||
|
[Authorize("BookStore.Books.Create")] |
||||
|
public async Task<BookDto> CreateAsync(CreateBookDto input) |
||||
|
{ |
||||
|
//logic here |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
> Notice that you can use the `[Authorize]` attribute at both class and method levels. In the example above, the `CreateAsync` method is marked with the `[Authorize]` attribute, so it will check the user's permission before executing the method. Since the application service class also has a permission requirement, both permissions must be granted to the user to execute the method! |
||||
|
|
||||
|
And here is an example of how to check permissions in a controller: |
||||
|
|
||||
|
```csharp |
||||
|
[Authorize("BookStore.Books")] |
||||
|
public class CreateBookController : AbpController |
||||
|
{ |
||||
|
//omitted for brevity... |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Programmatic Permission Checking |
||||
|
|
||||
|
To conditionally control authorization in your code, you can use the `IAuthorizationService` service: |
||||
|
|
||||
|
```csharp |
||||
|
public class BookAppService : ApplicationService, IBookAppService |
||||
|
{ |
||||
|
public async Task<BookDto> CreateAsync(CreateBookDto input) |
||||
|
{ |
||||
|
// Checks the permission and throws an exception if the user does not have the permission |
||||
|
await AuthorizationService.CheckAsync(BookStorePermissions.Books.Create); |
||||
|
|
||||
|
// Your logic here |
||||
|
} |
||||
|
|
||||
|
public async Task<bool> CanUserCreateBooksAsync() |
||||
|
{ |
||||
|
// Checks if the permission is granted for the current user |
||||
|
return await AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Create); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
You can use the `IAuthorizationService`'s helpful methods for authorization checking, as shown in the example above: |
||||
|
|
||||
|
- `IsGrantedAsync` checks if the current user has the given permission. |
||||
|
- `CheckAsync` throws an exception if the current user does not have the given permission. |
||||
|
- `AuthorizeAsync` checks if the current user has the given permission and returns an `AuthorizationResult`, which has a `Succeeded` property that you can use to verify if the user has the permission. |
||||
|
|
||||
|
Also notice that we did not inject the `IAuthorizationService` in the constructor, because we are using the `ApplicationService` base class, which already provides property injection for it. This means we can directly use it in our application services, just like other helpful base services (such as `ICurrentUser` and `ICurrentTenant`). |
||||
|
|
||||
|
## Conclusion |
||||
|
|
||||
|
Permission-based authorization in ABP Framework provides a powerful and flexible approach to securing your applications. By building on ASP.NET Core's policy-based authorization, ABP offers a clean abstraction that simplifies permission management while maintaining the full power of the underlying system. |
||||
|
|
||||
|
The ability to check permissions in both application services and controllers makes ABP Framework's authorization system very flexible and powerful, yet easy to use. |
||||
|
|
||||
|
Additionally, the Permission Management Module makes it very easy to manage permissions and roles through the UI. You can learn more about how it works in the [documentation](https://abp.io/docs/latest/modules/permission-management). |
||||
|
After Width: | Height: | Size: 324 KiB |
|
After Width: | Height: | Size: 25 KiB |
@ -0,0 +1,169 @@ |
|||||
|
# Best Practices for Designing Backward‑Compatible REST APIs in a Microservice Solution for .NET Developers |
||||
|
|
||||
|
## Introduction |
||||
|
With microservice architecture, each service develops and ships independently at its own pace, and clients infrequently update in lockstep. **Backward compatibility** means that when you release new versions, current consumers continue to function without changing code. This article provides a practical, 6–7 minute tutorial specific to **.NET developers**. |
||||
|
|
||||
|
--- |
||||
|
## What Counts as “Breaking”? (and what doesn’t) |
||||
|
A change is **breaking** if a client that previously conformed can **fail at compile time or runtime**, or exhibit **different business‑critical behavior**, **without** changing that client in any way. In other words: if an old client needs to be altered in order to continue functioning as it did, your change is breaking. |
||||
|
|
||||
|
### Examples of breaking changes |
||||
|
- **Deleting or renaming an endpoint** or modifying its URL/route. |
||||
|
- **Making an existing field required** (e.g., requiring `address`). |
||||
|
- **Data type or format changes** (e.g., `price: string` → `price: number`, or date format changes). |
||||
|
- **Altering default behavior or ordering** that clients implicitly depend on (hidden contracts). |
||||
|
- **Changing the error model** or HTTP status codes in a manner that breaks pre-existing error handling. |
||||
|
- **Renaming fields** or **making optional fields required** in requests or responses. |
||||
|
- **Reinterpreting semantics** (e.g., `status="closed"` formerly included archived items, but no longer does). |
||||
|
|
||||
|
### Examples of non‑breaking changes |
||||
|
- **Optional fields or query parameters can be added** (clients may disregard them). |
||||
|
- **Adding new enum values** (if the clients default to a safe behavior for unrecognized values). |
||||
|
- **Adding a new endpoint** while leaving the previous one unchanged. |
||||
|
- **Performance enhancements** that leave input/output unchanged. |
||||
|
- **Including metadata** (e.g., pagination links) without changing the current payload shape. |
||||
|
|
||||
|
> Golden rule: **Old clients should continue to work exactly as they did before—without any changes.** |
||||
|
|
||||
|
--- |
||||
|
## Versioning Strategy |
||||
|
Versioning is your master control lever for managing change. Typical methods: |
||||
|
|
||||
|
1) **URI Segment** (simplest) |
||||
|
``` |
||||
|
GET /api/v1/orders |
||||
|
GET /api/v2/orders |
||||
|
``` |
||||
|
Pros: Cache/gateway‑friendly; explicit in docs. Cons: URL noise. |
||||
|
|
||||
|
2) **Header‑Based** |
||||
|
``` |
||||
|
GET /api/orders |
||||
|
x-api-version: 2.0 |
||||
|
``` |
||||
|
Pros: Clean URLs; multiple reader support. Cons: Needs proxy/CDN rules. |
||||
|
|
||||
|
3) **Media Type** |
||||
|
Accept: application/json;v=2 |
||||
|
|
||||
|
Pros: Semantically accurate. <br> Cons: More complicated to test and implement. <br> **Recommendation:** For the majority of teams, favor **URI segments**, with an optional **`x-api-version`** header for flexibility. |
||||
|
|
||||
|
### Quick Setup in ASP.NET Core (Asp.Versioning) |
||||
|
```csharp |
||||
|
// Program.cs |
||||
|
using Asp.Versioning; |
||||
|
|
||||
|
builder.Services.AddControllers(); |
||||
|
builder.Services.AddApiVersioning(o => |
||||
|
{ |
||||
|
o.DefaultApiVersion = new ApiVersion(1, 0); |
||||
|
o.AssumeDefaultVersionWhenUnspecified = true; |
||||
|
o.ReportApiVersions = true; // response header: api-supported-versions |
||||
|
o.ApiVersionReader = ApiVersionReader.Combine( |
||||
|
new UrlSegmentApiVersionReader(), |
||||
|
new HeaderApiVersionReader("x-api-version") |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
builder.Services.AddVersionedApiExplorer(o => |
||||
|
{ |
||||
|
o.GroupNameFormat = "'v'VVV"; // v1, v2 |
||||
|
o.SubstituteApiVersionInUrl = true; |
||||
|
}); |
||||
|
``` |
||||
|
```csharp |
||||
|
// Controller |
||||
|
using Asp.Versioning; |
||||
|
|
||||
|
[ApiController] |
||||
|
[Route("api/v{version:apiVersion}/orders")] |
||||
|
public class OrdersController : ControllerBase |
||||
|
{ |
||||
|
[HttpGet] |
||||
|
[ApiVersion("1.0", Deprecated = true)] |
||||
|
public IActionResult GetV1() => Ok(new { message = "v1" }); |
||||
|
|
||||
|
[HttpGet] |
||||
|
[MapToApiVersion("2.0")] |
||||
|
public IActionResult GetV2() => Ok(new { message = "v2", includes = new []{"items"} }); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
## Schema Evolution Playbook (JSON & DTO) |
||||
|
Obey the following rules for compatibility‑safe evolution: |
||||
|
|
||||
|
- **Add‑only changes**: Favor adding **optional** fields; do not remove/rename fields. |
||||
|
- **Maintain defaults**: When the new field is disregarded, the old functionality must not change. |
||||
|
- **Enum extension**: Clients should handle unknown enum values gracefully (default behavior). |
||||
|
- **Deprecation pipeline**: Mark fields/endpoints as deprecated **at least one version** prior to removal and publicize extensively. - **Stability by contract**: Record any unspoken contracts (ordering, casing, formats) that clients depend on. |
||||
|
|
||||
|
### Example: adding a non‑breaking field |
||||
|
```csharp |
||||
|
public record OrderDto( |
||||
|
Guid Id, |
||||
|
decimal Total, |
||||
|
string Currency, |
||||
|
string? SalesChannel // new, optional |
||||
|
); |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
## Compatibility‑Safe API Behaviors |
||||
|
- **Error model**: Use a standard structure (e.g., RFC 7807 `ProblemDetails`). Avoid ad‑hoc error shapes on a per-endpoint basis. |
||||
|
- **Versioning/Deprecation communication** through headers: |
||||
|
- `api-supported-versions: 1.0, 2.0` |
||||
|
- `Deprecation: true` (in deprecated endpoints) |
||||
|
- `Sunset: Wed, 01 Oct 2025 00:00:00 GMT` (planned deprecation date) |
||||
|
- **Idempotency**: Use an `Idempotency-Key` header for retry-safe POSTs. |
||||
|
- **Optimistic concurrency**: Utilize `ETag`/`If-Match` to prevent lost updates. |
||||
|
- **Pagination**: Prefer cursor tokens (`nextPageToken`) to protect clients from sorting/index changes. |
||||
|
- **Time**: Employ ISO‑8601 in UTC; record time‑zone semantics and rounding conventions. |
||||
|
|
||||
|
--- |
||||
|
## Rollout & Deprecation Policy |
||||
|
A good deprecation policy is **announce → coexist → remove**: |
||||
|
|
||||
|
1) **Announce**: Release changelog, docs, and comms (mail/Slack) with v2 information and the sunset date. |
||||
|
2) **Coexist**: Operate v1 and v2 side by side. Employ gateway percentage routing for progressive cutover. |
||||
|
3) **Observability**: Monitor errors/latency/usage **by version**. When v1 traffic falls below ~5%, plan for removal. 4) **Remove**: Post sunset date, return **410 (Gone)** with a link to migration documentation. |
||||
|
|
||||
|
**Canary & Blue‑Green**: Initialize v2 with a small traffic portion and compare error/latency budgets prior to scaling up. |
||||
|
|
||||
|
--- |
||||
|
## Contract & Compatibility Testing |
||||
|
- **Consumer‑Driven Contracts**: Write expectations using Pact.NET; verify at provider CI. |
||||
|
- **Golden files / snapshots**: Freeze representative JSON payloads and automatically detect regressions. |
||||
|
- **Version-specific smoke tests**: Maintain separate, minimal test suites for v1 and v2. |
||||
|
- **SemVer discipline**: Minor = backward‑compatible; Major = breaking (avoid when possible). |
||||
|
|
||||
|
Minimal example (xUnit + snapshot style): |
||||
|
```csharp |
||||
|
[Fact] |
||||
|
public async Task Orders_v1_contract_should_match_snapshot() |
||||
|
{ |
||||
|
var resp = await _client.GetStringAsync("/api/v1/orders"); |
||||
|
Approvals.VerifyJson(resp); // snapshot comparison |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
## Tooling & Docs (for .NET) |
||||
|
- **Asp.Versioning (NuGet)**: API versioning + ApiExplorer integration. |
||||
|
- **Swashbuckle / NSwag**: Generate an OpenAPI definition **for every version** (`/swagger/v1/swagger.json`, `/swagger/v2/swagger.json`). Display both in Swagger UI. |
||||
|
- **Polly**: Client‑side retries/fallbacks to handle transient failures and ensure resilience. |
||||
|
- **Serilog + OpenTelemetry**: Collect metrics/logs/traces by version for observability and SLOs. |
||||
|
|
||||
|
Swagger UI configuration by group name: |
||||
|
```csharp |
||||
|
app.UseSwagger(); |
||||
|
app.UseSwaggerUI(c => |
||||
|
{ |
||||
|
c.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1"); |
||||
|
c.SwaggerEndpoint("/swagger/v2/swagger.json", "API v2"); |
||||
|
}); |
||||
|
``` |
||||
|
--- |
||||
|
|
||||
|
## Conclusion |
||||
|
Backward compatibility is not a version number—it is **disciplined change management**. When you use add‑only schema evolution, a well‑defined versioning strategy, strict contract testing, and rolling rollout, you maintain microservice independence and safeguard consumer experience. |
||||
|
After Width: | Height: | Size: 755 KiB |
@ -0,0 +1,29 @@ |
|||||
|
# IMPROVE YOUR ABP SKILLS WITH 33% OFF LIVE TRAININGS! |
||||
|
|
||||
|
We have exciting news to share\! As you know, we offer live training packages to help you improve your skills and knowledge of ABP. From September 8th to 19th, we are giving you 33% OFF our live trainings, so you can learn more about the product at a discounted price\! |
||||
|
|
||||
|
#### Why Join ABP.IO Training? |
||||
|
|
||||
|
ABP training programs are designed to help developers, architects, and teams master the ABP Framework efficiently. Whether you're new to the framework or looking to deepen your knowledge, our courses cover everything you need to build robust and scalable applications with ABP. |
||||
|
|
||||
|
#### What You’ll Gain: |
||||
|
|
||||
|
✔ Comprehensive live training from ABP Experts |
||||
|
✔ Hands-on learning with real-world applications |
||||
|
✔ Best practices for building modern web applications |
||||
|
✔ Certification to showcase your expertise |
||||
|
|
||||
|
#### [Limited-Time 33% Discount – Don’t Miss Out\!](https://abp.io/trainings?utm_source=referral&utm_medium=website&utm_campaign=training_abpblogpost) |
||||
|
|
||||
|
For a short period, all training packages are available at a 33% discount. This is a great opportunity to upskill yourself or train your team at a significantly reduced cost. |
||||
|
|
||||
|
#### How to Get the Discount? |
||||
|
|
||||
|
Simply visit our training page, select your preferred package, add your note if needed and send your training request, that's all\! ABP Training Team will reply to your request via email soon. |
||||
|
|
||||
|
#### Take Advantage of This Offer Today |
||||
|
|
||||
|
Invest in your skills and advance your career with ABP.IO training. This offer won’t last long, so grab your spot now\! |
||||
|
|
||||
|
### 🔗[Sign up for training now and start building with ABP](https://abp.io/trainings?utm_source=referral&utm_medium=website&utm_campaign=training_abpblogpost)\! |
||||
|
|
||||
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 310 KiB |
@ -0,0 +1,335 @@ |
|||||
|
# Keep Track of Your Users in an ASP.NET Core Application |
||||
|
|
||||
|
Tracking what users do in your app matters for security, debugging, and business insights. Doing it by hand usually means lots of boilerplate: managing request context, logging operations, tracking entity changes, and more. It adds complexity and makes mistakes more likely. |
||||
|
|
||||
|
## Why Applications Need Audit Logs |
||||
|
|
||||
|
Audit logs are time-ordered records that show what happened in your app. |
||||
|
|
||||
|
A good audit log should capture details for every web request, including: |
||||
|
|
||||
|
### 1. Request and Response Details |
||||
|
- Basic info like **URL, HTTP method, browser**, and **HTTP status code** |
||||
|
- Network info like **client IP address** and **user agent** |
||||
|
- **Request parameters** and **response content** when needed |
||||
|
|
||||
|
### 2. Operations Performed |
||||
|
- **Controller actions** and **application service method calls** with parameters |
||||
|
- **Execution time** and **duration** for performance tracking |
||||
|
- **Call chains** and **dependencies** where helpful |
||||
|
|
||||
|
### 3. Entity Changes |
||||
|
- **Entity changes** that happen during requests |
||||
|
- **Property-level changes**, with old and new values |
||||
|
- **Change types** (create, update, delete) and timestamps |
||||
|
|
||||
|
### 4. Exception Information |
||||
|
- **Errors and exceptions** during request execution |
||||
|
- **Exception stack traces** and **error context** |
||||
|
- Clear records of failed operations |
||||
|
|
||||
|
### 5. Request Duration |
||||
|
- Key metrics for **measuring performance** |
||||
|
- **Finding bottlenecks** and optimization opportunities |
||||
|
- Useful data for **monitoring system health** |
||||
|
|
||||
|
## The Challenge with Doing It by Hand |
||||
|
|
||||
|
In ASP.NET Core, developers often use middleware or MVC filters for tracking. Here’s what that looks like and the common problems you’ll hit. |
||||
|
|
||||
|
### Using Middleware |
||||
|
|
||||
|
Middleware are components in the ASP.NET Core pipeline that run during request processing. |
||||
|
|
||||
|
Manual tracking typically requires: |
||||
|
- Writing custom middleware to intercept HTTP requests |
||||
|
- Extracting user info (user ID, username, IP address, and so on) |
||||
|
- Recording request start time and execution duration |
||||
|
- Handling both success and failure cases |
||||
|
- Saving audit data to logs or a database |
||||
|
|
||||
|
### Tracking Inside Business Methods |
||||
|
|
||||
|
In your business code, you also need to: |
||||
|
- Log the start and end of important operations |
||||
|
- Capture errors and related context |
||||
|
- Link business operations to the request-level audit data |
||||
|
- Make sure you track all critical actions |
||||
|
|
||||
|
### Problems with Manual Tracking |
||||
|
|
||||
|
Manual tracking has some big downsides: |
||||
|
|
||||
|
**Code duplication and maintenance pain**: Each controller ends up repeating similar tracking logic. Changing the rules means touching many places, and it’s easy to miss some. |
||||
|
|
||||
|
**Consistency and reliability issues**: Different people implement tracking differently. Exception paths are easy to forget. It’s hard to ensure complete coverage. |
||||
|
|
||||
|
**Performance and scalability concerns**: Homegrown tracking can slow the app if not designed well. Tuning and extending it takes effort. |
||||
|
|
||||
|
**Entity change tracking is especially hard**. It often requires: |
||||
|
- Recording original values before updates |
||||
|
- Comparing old and new values for each property |
||||
|
- Handling complex types, collections, and navigation properties |
||||
|
- Designing and saving change records |
||||
|
- Capturing data even when exceptions happen |
||||
|
|
||||
|
This usually leads to: |
||||
|
- **A lot of code** in every update method |
||||
|
- **Easy-to-miss edge cases** and subtle bugs |
||||
|
- **High maintenance** when entity models change |
||||
|
- **Extra queries and comparisons** that can hurt performance |
||||
|
- **Incomplete coverage** for complex scenarios |
||||
|
|
||||
|
## ABP Framework’s Built-in Solution |
||||
|
|
||||
|
ABP Framework includes a built-in audit logging system. It solves the problems above and adds useful features on top. |
||||
|
|
||||
|
### Simple Setup vs. Manual Tracking |
||||
|
|
||||
|
Instead of writing lots of code, you configure it once: |
||||
|
|
||||
|
```csharp |
||||
|
// Configure audit log options in the module's ConfigureServices method |
||||
|
Configure<AbpAuditingOptions>(options => |
||||
|
{ |
||||
|
options.IsEnabled = true; // Enable audit log system (default value) |
||||
|
options.IsEnabledForAnonymousUsers = true; // Track anonymous users (default value) |
||||
|
options.IsEnabledForGetRequests = false; // Skip GET requests (default value) |
||||
|
options.AlwaysLogOnException = true; // Always log on errors (default value) |
||||
|
options.HideErrors = true; // Hide audit log errors (default value) |
||||
|
options.EntityHistorySelectors.AddAllEntities(); // Track all entity changes |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
```csharp |
||||
|
// Add middleware in the module's OnApplicationInitialization method |
||||
|
public override void OnApplicationInitialization(ApplicationInitializationContext context) |
||||
|
{ |
||||
|
var app = context.GetApplicationBuilder(); |
||||
|
|
||||
|
// Add audit log middleware - one line of code solves all problems! |
||||
|
app.UseAuditing(); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
By contrast, manual tracking needs middleware, controller logic, exception handling, and often hundreds of lines. With ABP, a couple of lines enable it and it just works. |
||||
|
|
||||
|
## What You Get with ABP |
||||
|
|
||||
|
Here’s how ABP removes tracking code from your application and still captures what you need. |
||||
|
|
||||
|
### 1. Application Services: No Tracking Code |
||||
|
|
||||
|
Manual approach: You’d log inside each method and still risk missing cases. |
||||
|
|
||||
|
ABP approach: Tracking is automatic—no tracking code in your methods. |
||||
|
|
||||
|
```csharp |
||||
|
public class BookAppService : ApplicationService |
||||
|
{ |
||||
|
private readonly IRepository<Book, Guid> _bookRepository; |
||||
|
private readonly IRepository<Author, Guid> _authorRepository; |
||||
|
|
||||
|
[Authorize(BookPermissions.Create)] |
||||
|
public virtual async Task<BookDto> CreateAsync(CreateBookDto input) |
||||
|
{ |
||||
|
// No need to write any tracking code! |
||||
|
// ABP automatically tracks: |
||||
|
// - Method calls and parameters |
||||
|
// - Calling user |
||||
|
// - Execution duration |
||||
|
// - Any exceptions thrown |
||||
|
|
||||
|
var author = await _authorRepository.GetAsync(input.AuthorId); |
||||
|
var book = new Book(input.Title, author, input.Price); |
||||
|
|
||||
|
await _bookRepository.InsertAsync(book); |
||||
|
|
||||
|
return ObjectMapper.Map<Book, BookDto>(book); |
||||
|
} |
||||
|
|
||||
|
[Authorize(BookPermissions.Update)] |
||||
|
public virtual async Task<BookDto> UpdateAsync(Guid id, UpdateBookDto input) |
||||
|
{ |
||||
|
var book = await _bookRepository.GetAsync(id); |
||||
|
|
||||
|
// No need to write any entity change tracking code! |
||||
|
// ABP automatically tracks entity changes: |
||||
|
// - Which properties changed |
||||
|
// - Old and new values |
||||
|
// - When the change happened |
||||
|
|
||||
|
book.ChangeTitle(input.Title); |
||||
|
book.ChangePrice(input.Price); |
||||
|
|
||||
|
await _bookRepository.UpdateAsync(book); |
||||
|
|
||||
|
return ObjectMapper.Map<Book, BookDto>(book); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
With manual code, each method might need 20–30 lines for tracking. With ABP, it’s zero—and you still get richer data. |
||||
|
|
||||
|
For entity changes, ABP also saves you from writing comparison code. It handles: |
||||
|
- Property change detection |
||||
|
- Recording old and new values |
||||
|
- Complex types and collections |
||||
|
- Navigation property changes |
||||
|
- All with no extra code to maintain |
||||
|
|
||||
|
### 2. Entity Change Tracking: One Line to Turn It On |
||||
|
|
||||
|
Manual approach: You’d compare properties, serialize complex types, track collection changes, and write to storage. |
||||
|
|
||||
|
ABP approach: Mark the entity or select entities globally. |
||||
|
|
||||
|
```csharp |
||||
|
// Enable audit log for specific entity - one line of code solves all problems! |
||||
|
[Audited] |
||||
|
public class MyEntity : Entity<Guid> |
||||
|
{ |
||||
|
public string Name { get; set; } |
||||
|
public string Description { get; set; } |
||||
|
|
||||
|
[DisableAuditing] // Exclude sensitive data - security control |
||||
|
public string InternalNotes { get; set; } |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
```csharp |
||||
|
// Or global configuration - batch processing |
||||
|
Configure<AbpAuditingOptions>(options => |
||||
|
{ |
||||
|
// Track all entities - one line of code tracks all entity changes |
||||
|
options.EntityHistorySelectors.AddAllEntities(); |
||||
|
|
||||
|
// Or use custom selector - precise control |
||||
|
options.EntityHistorySelectors.Add( |
||||
|
new NamedTypeSelector( |
||||
|
"MySelectorName", |
||||
|
type => typeof(IEntity).IsAssignableFrom(type) |
||||
|
) |
||||
|
); |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
### 3. Extension Features |
||||
|
|
||||
|
Manual approach: Adding custom tracking usually spreads across many places and is hard to test. |
||||
|
|
||||
|
ABP approach: Use a contributor for clean, centralized extensions. |
||||
|
|
||||
|
```csharp |
||||
|
public class MyAuditLogContributor : AuditLogContributor |
||||
|
{ |
||||
|
public override void PreContribute(AuditLogContributionContext context) |
||||
|
{ |
||||
|
var currentUser = context.ServiceProvider.GetRequiredService<ICurrentUser>(); |
||||
|
|
||||
|
// Easily add custom properties - manual implementation needs lots of work |
||||
|
context.AuditInfo.SetProperty( |
||||
|
"MyCustomClaimValue", |
||||
|
currentUser.FindClaimValue("MyCustomClaim") |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
public override void PostContribute(AuditLogContributionContext context) |
||||
|
{ |
||||
|
// Add custom comments - business logic integration |
||||
|
context.AuditInfo.Comments.Add("Some comment..."); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Register contributor - one line of code enables extension features |
||||
|
Configure<AbpAuditingOptions>(options => |
||||
|
{ |
||||
|
options.Contributors.Add(new MyAuditLogContributor()); |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
### 4. Precise Control |
||||
|
|
||||
|
Manual approach: You end up with complex conditional logic. |
||||
|
|
||||
|
ABP approach: Use attributes for simple, precise control. |
||||
|
|
||||
|
```csharp |
||||
|
// Disable audit log for specific controller - precise control |
||||
|
[DisableAuditing] |
||||
|
public class HomeController : AbpController |
||||
|
{ |
||||
|
// Health check endpoints won't be audited - avoid meaningless logs |
||||
|
} |
||||
|
|
||||
|
// Disable for specific action - method-level control |
||||
|
public class HomeController : AbpController |
||||
|
{ |
||||
|
[DisableAuditing] |
||||
|
public async Task<ActionResult> Home() |
||||
|
{ |
||||
|
// This action won't be audited - public data access |
||||
|
} |
||||
|
|
||||
|
public async Task<ActionResult> OtherActionLogged() |
||||
|
{ |
||||
|
// This action will be audited - important business operation |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### 5. Visual Management of Audit Logs |
||||
|
|
||||
|
ABP also provides a UI to browse and inspect audit logs: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## Manual vs. ABP: A Quick Comparison |
||||
|
|
||||
|
The benefits of ABP’s audit log system compared to doing it by hand: |
||||
|
|
||||
|
| Aspect | Manual Implementation | ABP Audit Logs | |
||||
|
|--------|----------------------|----------------| |
||||
|
| **Setup Complexity** | High — Write middleware, services, repository code | Low — A few lines of config, works out of the box | |
||||
|
| **Code Maintenance** | High — Tracking code spread across the app | Low — Centralized, convention-based | |
||||
|
| **Consistency** | Variable — Depends on discipline | Consistent — Automated and standardized | |
||||
|
| **Performance** | Risky without careful tuning | Built-in optimizations and scope control | |
||||
|
| **Functionality Completeness** | Basic tracking only | Comprehensive by default | |
||||
|
| **Error Handling** | Easy to miss edge cases | Automatic and reliable | |
||||
|
| **Data Integrity** | Manual effort required | Handled by the framework | |
||||
|
| **Extensibility** | Custom work is costly | Rich extension points | |
||||
|
| **Development Efficiency** | Weeks to build | Minutes to enable | |
||||
|
| **Learning Cost** | Understand many details | Convention-based, low effort | |
||||
|
|
||||
|
## Why ABP Audit Logs Matter |
||||
|
|
||||
|
ABP’s audit logging removes the boilerplate from user tracking in ASP.NET Core apps. |
||||
|
|
||||
|
### Core Idea |
||||
|
|
||||
|
Manual tracking is error-prone and hard to maintain. ABP gives you a convention-based, automated system that works with minimal setup. |
||||
|
|
||||
|
### Key Benefits |
||||
|
|
||||
|
ABP runs by convention, so you don’t need repetitive code. You can control behavior at the request, entity, and method levels. It automatically captures request details, operations, entity changes, and exceptions, and you can extend it with contributors when needed. |
||||
|
|
||||
|
### Results in Practice |
||||
|
|
||||
|
| Metric | Manual Implementation | ABP Implementation | Improvement | |
||||
|
|--------|----------------------|-------------------|-------------| |
||||
|
| Development Time | Weeks | Minutes | **99%+** | |
||||
|
| Lines of Code | Hundreds of lines | 2 lines of config | **99%+** | |
||||
|
| Maintenance Cost | High | Low | **Significant** | |
||||
|
| Functionality Completeness | Basic | Comprehensive | **Significant** | |
||||
|
| Error Rate | Higher risk | Lower risk | **Improved** | |
||||
|
|
||||
|
### Recommendation |
||||
|
|
||||
|
If you need audit logs, start with ABP’s built-in system. It reduces effort, improves consistency, and stays flexible as your app grows. You can focus on your business logic and let the framework handle the infrastructure. |
||||
|
|
||||
|
## References |
||||
|
|
||||
|
- [ABP Audit Logging](https://abp.io/docs/latest/framework/infrastructure/audit-logging) |
||||
|
- [ABP Audit Logging UI](https://abp.io/modules/Volo.AuditLogging.Ui) |
||||
|
After Width: | Height: | Size: 520 KiB |
@ -1,7 +0,0 @@ |
|||||
# Dynamic Proxying / Interceptors |
|
||||
|
|
||||
This document is planned to be written later. |
|
||||
|
|
||||
## See Also |
|
||||
|
|
||||
* [Video tutorial](https://abp.io/video-courses/essentials/interception) |
|
||||
@ -0,0 +1,213 @@ |
|||||
|
# Interceptors |
||||
|
|
||||
|
ABP provides a powerful interception system that allows you to execute custom logic before and after method calls without modifying the original method code. This is achieved through **dynamic proxying** and is extensively used throughout the ABP framework to implement cross-cutting concerns. ABP's interception is implemented on top of the [Castle DynamicProxy](https://www.castleproject.org/projects/dynamicproxy/) library. |
||||
|
|
||||
|
## What is Dynamic Proxying / Interception? |
||||
|
|
||||
|
**Interception** is a technique that allows executing additional logic before or after a method call without directly modifying the method's code. This is achieved through **dynamic proxying**, where the runtime generates proxy classes that wrap the original class. |
||||
|
|
||||
|
When a method is called on a proxied object: |
||||
|
1. The call is intercepted by the proxy |
||||
|
2. Custom behaviors (like logging, validation, or authorization) can be executed |
||||
|
3. The original method is called |
||||
|
4. Additional logic can be executed after the method completes |
||||
|
|
||||
|
This enables **cross-cutting concerns** (logic that applies across many parts of an application) to be handled in a clean, reusable way without code duplication. |
||||
|
|
||||
|
## Similarities and Differences with MVC Action/Page Filters |
||||
|
|
||||
|
If you are familiar with ASP.NET Core MVC, you've likely used **action filters** or **page filters**. Interceptors are conceptually similar but have some key differences: |
||||
|
|
||||
|
### Similarities |
||||
|
|
||||
|
* Both allow executing code before and after method execution |
||||
|
* Both are used to implement cross-cutting concerns like validation, logging, caching, or exception handling |
||||
|
* Both support asynchronous operations |
||||
|
|
||||
|
### Differences |
||||
|
|
||||
|
* **Scope**: Filters are tied to MVC's request pipeline, while interceptors can be applied to **any class or service** in the application |
||||
|
* **Configuration**: Filters are configured via attributes or middleware in MVC, while interceptors in ABP are applied through **dependency injection and dynamic proxies** |
||||
|
* **Target**: Interceptors can target application services, domain services, repositories, and virtually any service resolved from the IoC container—not just web controllers |
||||
|
|
||||
|
## How ABP Uses Interceptors |
||||
|
|
||||
|
ABP Framework extensively leverages interception to provide built-in features without requiring boilerplate code. Here are some key examples: |
||||
|
|
||||
|
### [Unit of Work (UOW)](../architecture/domain-driven-design/unit-of-work.md) |
||||
|
|
||||
|
Automatically begins and commits/rolls back a database transaction when entering or exiting an application service method. This ensures data consistency without manual transaction management. |
||||
|
|
||||
|
### [Input Validation](../fundamentals/validation.md) |
||||
|
|
||||
|
Input DTOs are automatically validated against data annotation attributes and custom validation rules before executing the service logic, providing consistent validation behavior across all services. |
||||
|
|
||||
|
### [Authorization](../fundamentals/authorization.md) |
||||
|
|
||||
|
Checks user permissions before allowing the execution of application service methods, ensuring security policies are enforced consistently. |
||||
|
|
||||
|
### [Feature](./features.md) & [Global Feature](./global-features.md) Checking |
||||
|
|
||||
|
Checks if a feature is enabled before executing the service logic, allowing you to conditionally enable or restrict functionality for tenants or the application. |
||||
|
|
||||
|
### [Auditing](./audit-logging.md) |
||||
|
|
||||
|
Automatically logs who performed an action, when it happened, what parameters were used, and what data was involved, providing comprehensive audit trails. |
||||
|
|
||||
|
## Building Your Own Interceptor |
||||
|
|
||||
|
You can create custom interceptors in ABP to implement your own cross-cutting concerns. |
||||
|
|
||||
|
### Creating an Interceptor |
||||
|
|
||||
|
Create a class that inherits from `AbpInterceptor`: |
||||
|
|
||||
|
````csharp |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Aspects; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.DynamicProxy; |
||||
|
|
||||
|
public class ExecutionTimeLogInterceptor : AbpInterceptor, ITransientDependency |
||||
|
{ |
||||
|
private readonly ILogger<ExecutionTimeLogInterceptor> _logger; |
||||
|
|
||||
|
public ExecutionTimeLogInterceptor(ILogger<ExecutionTimeLogInterceptor> logger) |
||||
|
{ |
||||
|
_logger = logger; |
||||
|
} |
||||
|
|
||||
|
public override async Task InterceptAsync(IAbpMethodInvocation invocation) |
||||
|
{ |
||||
|
var sw = Stopwatch.StartNew(); |
||||
|
|
||||
|
_logger.LogInformation($"Executing {invocation.TargetObject.GetType().Name}.{invocation.Method.Name}"); |
||||
|
|
||||
|
// Proceed to the actual target method |
||||
|
await invocation.ProceedAsync(); |
||||
|
|
||||
|
sw.Stop(); |
||||
|
|
||||
|
_logger.LogInformation($"Executed {invocation.TargetObject.GetType().Name}.{invocation.Method.Name} in {sw.ElapsedMilliseconds} ms"); |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
### Register Interceptors |
||||
|
|
||||
|
Create a static class that contains the `RegisterIfNeeded` method and register the interceptor in the `PreConfigureServices` method of your module. |
||||
|
|
||||
|
The `ShouldIntercept` method is used to determine if the interceptor should be registered for the given type. You can add an `IExecutionTimeLogEnabled` interface and implement it in the classes that you want to intercept. |
||||
|
|
||||
|
> `DynamicProxyIgnoreTypes` is static class that contains the types that should be ignored by the interceptor. See [Performance Considerations](#performance-considerations) for more information. |
||||
|
|
||||
|
````csharp |
||||
|
// Define an interface to mark the classes that should be intercepted |
||||
|
public interface IExecutionTimeLogEnabled |
||||
|
{ |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
````csharp |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
// A simple service that added to the DI container and will be intercepted since it implements the `IExecutionTimeLogEnabled` interface |
||||
|
public class SampleExecutionTimeService : IExecutionTimeLogEnabled, ITransientDependency |
||||
|
{ |
||||
|
public virtual async Task DoWorkAsync() |
||||
|
{ |
||||
|
// Simulate a long-running task to test the interceptor |
||||
|
await Task.Delay(1000); |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
````csharp |
||||
|
using System; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.DynamicProxy; |
||||
|
|
||||
|
public static class ExecutionTimeLogInterceptorRegistrar |
||||
|
{ |
||||
|
public static void RegisterIfNeeded(IOnServiceRegistredContext context) |
||||
|
{ |
||||
|
if (ShouldIntercept(context.ImplementationType)) |
||||
|
{ |
||||
|
context.Interceptors.TryAdd<ExecutionTimeLogInterceptor>(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static bool ShouldIntercept(Type type) |
||||
|
{ |
||||
|
return !DynamicProxyIgnoreTypes.Contains(type) && typeof(IExecutionTimeLogEnabled).IsAssignableFrom(type); |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
````csharp |
||||
|
public override void PreConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
context.Services.OnRegistered(ExecutionTimeLogInterceptorRegistrar.RegisterIfNeeded); |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
## Restrictions and Important Notes |
||||
|
|
||||
|
### Always use asynchronous methods |
||||
|
|
||||
|
For best performance and reliability, implement your service methods as asynchronous to avoid **async over sync**, that can cause unexpected problems, For more information, see [Should I expose synchronous wrappers for asynchronous methods?](https://devblogs.microsoft.com/dotnet/should-i-expose-synchronous-wrappers-for-asynchronous-methods/) |
||||
|
|
||||
|
### Virtual Methods Requirement |
||||
|
|
||||
|
For **class proxies**, methods need to be marked as `virtual` so that they can be overridden by the proxy. Otherwise, interception will not occur. |
||||
|
|
||||
|
````csharp |
||||
|
public class MyService : IExecutionTimeLogEnabled, ITransientDependency |
||||
|
{ |
||||
|
// This method CANNOT be intercepted (not virtual) |
||||
|
public void CannotBeIntercepted() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
// This method CAN be intercepted (virtual) |
||||
|
public virtual void CanBeIntercepted() |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
```` |
||||
|
|
||||
|
> This restriction does **not** apply to interface-based proxies. If your service implements an interface and is injected via that interface, all methods can be intercepted regardless of the `virtual` keyword. |
||||
|
|
||||
|
### Dependency Injection Scope |
||||
|
|
||||
|
Interceptors only work when services are resolved from the dependency injection container. Direct instantiation with `new` bypasses interception: |
||||
|
|
||||
|
````csharp |
||||
|
// This will NOT be intercepted |
||||
|
var service = new MyService(); |
||||
|
service.CannotBeIntercepted(); |
||||
|
|
||||
|
// This WILL be intercepted (if MyService is registered with DI) |
||||
|
var service = serviceProvider.GetService<MyService>(); |
||||
|
service.CanBeIntercepted(); |
||||
|
```` |
||||
|
|
||||
|
### Performance Considerations |
||||
|
|
||||
|
Interceptors are generally efficient, but each one adds method-call overhead. Keep the number of interceptors minimal on hot paths. |
||||
|
|
||||
|
Castle DynamicProxy can negatively impact performance for certain components, notably ASP.NET Core MVC controllers. See the discussions in [castleproject/Core#486](https://github.com/castleproject/Core/issues/486) and [abpframework/abp#3180](https://github.com/abpframework/abp/issues/3180). |
||||
|
|
||||
|
ABP uses interceptors for features like UOW, auditing, and authorization, which rely on dynamic proxy classes. For controllers, prefer implementing cross-cutting concerns with middleware or MVC/Page filters instead of dynamic proxies. |
||||
|
|
||||
|
To avoid generating dynamic proxies for specific types, use the static class `DynamicProxyIgnoreTypes` and add the base classes of the types to the list. Subclasses of any listed base class are also ignored. ABP framework already adds some base classes to the list (`ComponentBase, ControllerBase, PageModel, ViewComponent`); you can add more base classes if needed. |
||||
|
|
||||
|
> Always use interface-based proxies instead of class-based proxies for better performance. |
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
* [Video tutorial: Interceptors in ABP Framework](https://abp.io/video-courses/essentials/interception) |
||||
|
* [Castle DynamicProxy](https://www.castleproject.org/projects/dynamicproxy/) |
||||
|
* [Castle.Core.AsyncInterceptor](https://github.com/JSkimming/Castle.Core.AsyncInterceptor) |
||||
|
* [ASP.NET Core Filters](https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters) |
||||
@ -0,0 +1,47 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Application.Services; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using System.Linq; |
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using Volo.Abp.Internal.Telemetry.Activity; |
||||
|
using Volo.Abp.Internal.Telemetry.Activity.Contracts; |
||||
|
using Volo.Abp.Internal.Telemetry.Activity.Providers; |
||||
|
using Volo.Abp.Internal.Telemetry.Constants; |
||||
|
using Volo.Abp.Internal.Telemetry.Constants.Enums; |
||||
|
using Volo.Abp.Reflection; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Mvc; |
||||
|
|
||||
|
[ExposeServices(typeof(ITelemetryActivityEventEnricher), typeof(IHasParentTelemetryActivityEventEnricher<TelemetryApplicationInfoEnricher>))] |
||||
|
public sealed class TelemetryApplicationMetricsEnricher : TelemetryActivityEventEnricher, IHasParentTelemetryActivityEventEnricher<TelemetryApplicationInfoEnricher> |
||||
|
{ |
||||
|
private readonly ITypeFinder _typeFinder; |
||||
|
public TelemetryApplicationMetricsEnricher(ITypeFinder typeFinder, IServiceProvider serviceProvider) : base(serviceProvider) |
||||
|
{ |
||||
|
_typeFinder = typeFinder; |
||||
|
} |
||||
|
|
||||
|
protected override Task<bool> CanExecuteAsync(ActivityContext context) |
||||
|
{ |
||||
|
return Task.FromResult(context.SessionType == SessionType.ApplicationRuntime); |
||||
|
} |
||||
|
|
||||
|
protected override Task ExecuteAsync(ActivityContext context) |
||||
|
{ |
||||
|
var appServiceCount = _typeFinder.Types.Count(t => |
||||
|
typeof(IApplicationService).IsAssignableFrom(t) && |
||||
|
t is { IsAbstract: false, IsInterface: false } && |
||||
|
!t.AssemblyQualifiedName!.StartsWith(TelemetryConsts.VoloNameSpaceFilter)); |
||||
|
|
||||
|
var controllerCount = _typeFinder.Types.Count(t => |
||||
|
typeof(ControllerBase).IsAssignableFrom(t) && |
||||
|
!t.IsAbstract && |
||||
|
!t.AssemblyQualifiedName!.StartsWith(TelemetryConsts.VoloNameSpaceFilter)); |
||||
|
|
||||
|
|
||||
|
context.Current[ActivityPropertyNames.AppServiceCount] = appServiceCount; |
||||
|
context.Current[ActivityPropertyNames.ControllerCount] = controllerCount; |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
using System.Reflection; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using System.Runtime.InteropServices; |
||||
|
|
||||
|
// General Information about an assembly is controlled through the following
|
||||
|
// set of attributes. Change these attribute values to modify the information
|
||||
|
// associated with an assembly.
|
||||
|
[assembly: AssemblyConfiguration("")] |
||||
|
[assembly: AssemblyCompany("")] |
||||
|
[assembly: AssemblyProduct("Volo.Abp.Authorization.Abstractions")] |
||||
|
[assembly: AssemblyTrademark("")] |
||||
|
[assembly: InternalsVisibleTo("Volo.Abp.Authorization")] |
||||
|
|
||||
|
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
|
// to COM components. If you need to access a type in this assembly from
|
||||
|
// COM, set the ComVisible attribute to true on that type.
|
||||
|
[assembly: ComVisible(false)] |
||||
|
|
||||
|
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
|
[assembly: Guid("83632bec-2f60-4c2b-b964-30e8b37fa9d8")] |
||||
@ -0,0 +1,45 @@ |
|||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Internal.Telemetry.Activity; |
||||
|
using Volo.Abp.Internal.Telemetry.Activity.Contracts; |
||||
|
using Volo.Abp.Internal.Telemetry.Activity.Providers; |
||||
|
using Volo.Abp.Internal.Telemetry.Constants; |
||||
|
|
||||
|
namespace Volo.Abp.Authorization.Permissions; |
||||
|
|
||||
|
[ExposeServices(typeof(ITelemetryActivityEventEnricher), typeof(IHasParentTelemetryActivityEventEnricher<TelemetryApplicationInfoEnricher>))] |
||||
|
public sealed class TelemetryPermissionInfoEnricher : TelemetryActivityEventEnricher, IHasParentTelemetryActivityEventEnricher<TelemetryApplicationInfoEnricher> |
||||
|
{ |
||||
|
private readonly IPermissionDefinitionManager _permissionDefinitionManager; |
||||
|
|
||||
|
public TelemetryPermissionInfoEnricher(IPermissionDefinitionManager permissionDefinitionManager, |
||||
|
IServiceProvider serviceProvider) : base(serviceProvider) |
||||
|
{ |
||||
|
_permissionDefinitionManager = permissionDefinitionManager; |
||||
|
} |
||||
|
|
||||
|
protected override Task<bool> CanExecuteAsync(ActivityContext context) |
||||
|
{ |
||||
|
return Task.FromResult(context.ProjectId.HasValue); |
||||
|
} |
||||
|
|
||||
|
protected async override Task ExecuteAsync(ActivityContext context) |
||||
|
{ |
||||
|
var permissions = await _permissionDefinitionManager.GetPermissionsAsync(); |
||||
|
|
||||
|
var userDefinedPermissionsCount = permissions.Count(IsUserDefinedPermission); |
||||
|
|
||||
|
context.Current[ActivityPropertyNames.PermissionCount] = userDefinedPermissionsCount; |
||||
|
} |
||||
|
|
||||
|
private static bool IsUserDefinedPermission(PermissionDefinition permission) |
||||
|
{ |
||||
|
return permission.Properties.TryGetValue(PermissionDefinitionContext.KnownPropertyNames.CurrentProviderName, out var providerName) && |
||||
|
providerName is string && |
||||
|
!providerName.ToString()!.StartsWith(TelemetryConsts.VoloNameSpaceFilter); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
} |
||||
@ -0,0 +1,50 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Volo.Abp.Internal.Telemetry.Constants; |
||||
|
using Volo.Abp.Internal.Telemetry.Constants.Enums; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.Activity; |
||||
|
|
||||
|
public class ActivityContext |
||||
|
{ |
||||
|
public ActivityEvent Current { get; } |
||||
|
public Dictionary<string, object> ExtraProperties { get; } = new(); |
||||
|
public bool IsTerminated { get; private set; } |
||||
|
|
||||
|
public Guid? ProjectId => Current.Get<Guid?>(ActivityPropertyNames.ProjectId); |
||||
|
|
||||
|
public Guid? SolutionId => Current.Get<Guid?>(ActivityPropertyNames.SolutionId); |
||||
|
|
||||
|
public SessionType? SessionType => Current.Get<SessionType?>(ActivityPropertyNames.SessionType); |
||||
|
|
||||
|
public string? DeviceId => Current.Get<string?>(ActivityPropertyNames.DeviceId); |
||||
|
|
||||
|
public string? SolutionPath => ExtraProperties.TryGetValue(ActivityPropertyNames.SolutionPath, out var solutionPath) |
||||
|
? solutionPath?.ToString() |
||||
|
: null; |
||||
|
|
||||
|
private ActivityContext(ActivityEvent current) |
||||
|
{ |
||||
|
Current = current; |
||||
|
} |
||||
|
|
||||
|
public static ActivityContext Create(string activityName, string? details = null, |
||||
|
Action<Dictionary<string, object>>? additionalProperties = null) |
||||
|
{ |
||||
|
var activity = new ActivityEvent(activityName, details); |
||||
|
|
||||
|
if (additionalProperties is not null) |
||||
|
{ |
||||
|
var additionalPropertiesDict = new Dictionary<string, object>(); |
||||
|
activity[ActivityPropertyNames.AdditionalProperties] = additionalPropertiesDict; |
||||
|
additionalProperties.Invoke(additionalPropertiesDict); |
||||
|
} |
||||
|
|
||||
|
return new ActivityContext(activity); |
||||
|
} |
||||
|
|
||||
|
public void Terminate() |
||||
|
{ |
||||
|
IsTerminated = true; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,147 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Text.Json; |
||||
|
using Volo.Abp.Internal.Telemetry.Constants; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.Activity; |
||||
|
|
||||
|
public class ActivityEvent : Dictionary<string, object?> |
||||
|
{ |
||||
|
public ActivityEvent() |
||||
|
{ |
||||
|
this[ActivityPropertyNames.Id] = Guid.NewGuid(); |
||||
|
this[ActivityPropertyNames.Time] = DateTimeOffset.UtcNow; |
||||
|
} |
||||
|
|
||||
|
public ActivityEvent(string activityName, string? details = null) : this() |
||||
|
{ |
||||
|
Check.NotNullOrWhiteSpace(activityName, nameof(activityName)); |
||||
|
|
||||
|
this[ActivityPropertyNames.ActivityName] = activityName; |
||||
|
this[ActivityPropertyNames.ActivityDetails] = details; |
||||
|
} |
||||
|
|
||||
|
public bool HasSolutionInfo() |
||||
|
{ |
||||
|
return this.ContainsKey(ActivityPropertyNames.HasSolutionInfo); |
||||
|
} |
||||
|
|
||||
|
public bool HasDeviceInfo() |
||||
|
{ |
||||
|
return this.ContainsKey(ActivityPropertyNames.HasDeviceInfo); |
||||
|
} |
||||
|
|
||||
|
public bool HasProjectInfo() |
||||
|
{ |
||||
|
return this.ContainsKey(ActivityPropertyNames.HasProjectInfo); |
||||
|
} |
||||
|
|
||||
|
public T Get<T>(string key) |
||||
|
{ |
||||
|
return TryConvert<T>(key, out var value) ? value : default!; |
||||
|
} |
||||
|
|
||||
|
public bool TryGetValue<T>(string key, out T value) |
||||
|
{ |
||||
|
return TryConvert(key, out value); |
||||
|
} |
||||
|
|
||||
|
private bool TryConvert<T>(string key, out T result) |
||||
|
{ |
||||
|
result = default!; |
||||
|
if (!this.TryGetValue(key, out var value) || value is null) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
if (value is T tValue) |
||||
|
{ |
||||
|
result = tValue; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
if (value is JsonElement jsonElement) |
||||
|
{ |
||||
|
value = ExtractFromJsonElement(jsonElement); |
||||
|
if (value is null) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var targetType = typeof(T); |
||||
|
var underlyingType = Nullable.GetUnderlyingType(targetType) ?? targetType; |
||||
|
if (underlyingType.IsEnum) |
||||
|
{ |
||||
|
if (value is string str) |
||||
|
{ |
||||
|
result = (T)Enum.Parse(underlyingType, str, ignoreCase: true); |
||||
|
} |
||||
|
else if (value is int intValue) |
||||
|
{ |
||||
|
result = (T)Enum.ToObject(underlyingType, intValue); |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
if (underlyingType == typeof(Dictionary<string, object>[])) |
||||
|
{ |
||||
|
result = (T)value; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
if (underlyingType == typeof(Guid)) |
||||
|
{ |
||||
|
result = (T)(object)Guid.Parse(value.ToString()!); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
if (underlyingType == typeof(DateTimeOffset)) |
||||
|
{ |
||||
|
result = (T)(object)DateTimeOffset.Parse(value.ToString()!); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
// Nullable types
|
||||
|
result = (T)Convert.ChangeType(value, underlyingType); |
||||
|
return true; |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static object? ExtractFromJsonElement(JsonElement element) |
||||
|
{ |
||||
|
return element.ValueKind switch |
||||
|
{ |
||||
|
JsonValueKind.String => element.GetString(), |
||||
|
JsonValueKind.Number => element.GetInt32(), |
||||
|
JsonValueKind.True => true, |
||||
|
JsonValueKind.False => false, |
||||
|
JsonValueKind.Null => null, |
||||
|
JsonValueKind.Array => element.EnumerateArray() |
||||
|
.Select(item => |
||||
|
{ |
||||
|
if (item.ValueKind == JsonValueKind.Object) |
||||
|
{ |
||||
|
return item.EnumerateObject() |
||||
|
.ToDictionary(prop => prop.Name, prop => ExtractFromJsonElement(prop.Value)); |
||||
|
} |
||||
|
|
||||
|
return new Dictionary<string, object?> { { "value", ExtractFromJsonElement(item) } }; |
||||
|
}) |
||||
|
.ToArray(), |
||||
|
|
||||
|
JsonValueKind.Object => element.EnumerateObject() |
||||
|
.ToDictionary(prop => prop.Name, prop => ExtractFromJsonElement(prop.Value)), |
||||
|
_ => element.ToString() |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
using Volo.Abp.Internal.Telemetry.Activity.Providers; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.Activity.Contracts; |
||||
|
|
||||
|
public interface IHasParentTelemetryActivityEventEnricher<out TParent> where TParent: TelemetryActivityEventEnricher |
||||
|
{ |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.Activity.Contracts; |
||||
|
|
||||
|
public interface ITelemetryActivityEventBuilder |
||||
|
{ |
||||
|
Task<ActivityEvent?> BuildAsync(ActivityContext context); |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.Activity.Contracts; |
||||
|
|
||||
|
public interface ITelemetryActivityEventEnricher |
||||
|
{ |
||||
|
int ExecutionOrder { get; } |
||||
|
|
||||
|
Task EnrichAsync(ActivityContext context); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.Activity.Contracts; |
||||
|
|
||||
|
public interface ITelemetryActivityStorage |
||||
|
{ |
||||
|
Guid InitializeOrGetSession(); |
||||
|
void DeleteActivities(ActivityEvent[] activities); |
||||
|
void SaveActivity(ActivityEvent activityEvent); |
||||
|
List<ActivityEvent> GetActivities(); |
||||
|
bool ShouldAddDeviceInfo(); |
||||
|
bool ShouldAddSolutionInformation(Guid solutionId); |
||||
|
bool ShouldAddProjectInfo(Guid projectId); |
||||
|
bool ShouldSendActivities(); |
||||
|
void MarkActivitiesAsFailed(ActivityEvent[] activities); |
||||
|
} |
||||
@ -0,0 +1,49 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.DynamicProxy; |
||||
|
using Volo.Abp.Internal.Telemetry.Activity.Contracts; |
||||
|
using Volo.Abp.Internal.Telemetry.Constants; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.Activity.Providers; |
||||
|
|
||||
|
public class TelemetryActivityEventBuilder : ITelemetryActivityEventBuilder, ISingletonDependency |
||||
|
{ |
||||
|
private readonly List<ITelemetryActivityEventEnricher> _activityEnrichers; |
||||
|
|
||||
|
public TelemetryActivityEventBuilder(IEnumerable<ITelemetryActivityEventEnricher> activityDataEnrichers) |
||||
|
{ |
||||
|
_activityEnrichers = activityDataEnrichers |
||||
|
.Where(FilterEnricher) |
||||
|
.OrderByDescending(x => x.ExecutionOrder) |
||||
|
.ToList(); |
||||
|
} |
||||
|
public virtual async Task<ActivityEvent?> BuildAsync(ActivityContext context) |
||||
|
{ |
||||
|
foreach (var enricher in _activityEnrichers) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
await enricher.EnrichAsync(context); |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
//ignored
|
||||
|
} |
||||
|
|
||||
|
if (context.IsTerminated) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return context.Current; |
||||
|
} |
||||
|
|
||||
|
private static bool FilterEnricher(ITelemetryActivityEventEnricher enricher) |
||||
|
{ |
||||
|
return ProxyHelper.GetUnProxiedType(enricher).Assembly.FullName!.StartsWith(TelemetryConsts.VoloNameSpaceFilter) && |
||||
|
enricher is not IHasParentTelemetryActivityEventEnricher<TelemetryActivityEventEnricher>; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,70 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.DynamicProxy; |
||||
|
using Volo.Abp.Internal.Telemetry.Activity.Contracts; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.Activity.Providers; |
||||
|
|
||||
|
public abstract class TelemetryActivityEventEnricher : ITelemetryActivityEventEnricher, IScopedDependency |
||||
|
{ |
||||
|
public virtual int ExecutionOrder { get; set; } = 0; |
||||
|
protected bool IgnoreChildren { get; set; } |
||||
|
protected virtual Type? ReplaceParentType { get; set; } |
||||
|
|
||||
|
private readonly IServiceProvider _serviceProvider; |
||||
|
|
||||
|
protected TelemetryActivityEventEnricher(IServiceProvider serviceProvider) |
||||
|
{ |
||||
|
_serviceProvider = serviceProvider; |
||||
|
} |
||||
|
|
||||
|
public async Task EnrichAsync(ActivityContext context) |
||||
|
{ |
||||
|
if (!await CanExecuteAsync(context)) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
await ExecuteAsync(context); |
||||
|
await ExecuteChildrenAsync(context); |
||||
|
} |
||||
|
|
||||
|
protected virtual Task<bool> CanExecuteAsync(ActivityContext context) |
||||
|
{ |
||||
|
return Task.FromResult(true); |
||||
|
} |
||||
|
|
||||
|
protected abstract Task ExecuteAsync(ActivityContext context); |
||||
|
|
||||
|
protected virtual async Task ExecuteChildrenAsync(ActivityContext context) |
||||
|
{ |
||||
|
if (IgnoreChildren) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
using var scope = _serviceProvider.CreateScope(); |
||||
|
|
||||
|
foreach (var child in GetChildren(scope.ServiceProvider)) |
||||
|
{ |
||||
|
await child.EnrichAsync(context); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private ITelemetryActivityEventEnricher[] GetChildren(IServiceProvider serviceProvider) |
||||
|
{ |
||||
|
var targetType = ReplaceParentType ?? ProxyHelper.GetUnProxiedType(this); |
||||
|
var genericInterfaceType = typeof(IHasParentTelemetryActivityEventEnricher<>).MakeGenericType(targetType); |
||||
|
var enumerableType = typeof(IEnumerable<>).MakeGenericType(genericInterfaceType); |
||||
|
|
||||
|
var childServices = (IEnumerable<object>)serviceProvider.GetRequiredService(enumerableType); |
||||
|
|
||||
|
return childServices |
||||
|
.Cast<ITelemetryActivityEventEnricher>() |
||||
|
.ToArray(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,104 @@ |
|||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Reflection; |
||||
|
using System.Text.Json; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Internal.Telemetry.Activity.Contracts; |
||||
|
using Volo.Abp.Internal.Telemetry.Constants; |
||||
|
using Volo.Abp.Internal.Telemetry.Constants.Enums; |
||||
|
using Volo.Abp.Internal.Telemetry.Helpers; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.Activity.Providers; |
||||
|
|
||||
|
[ExposeServices(typeof(ITelemetryActivityEventEnricher), typeof(IHasParentTelemetryActivityEventEnricher<TelemetrySessionInfoEnricher>))] |
||||
|
public sealed class TelemetryApplicationInfoEnricher : TelemetryActivityEventEnricher, IHasParentTelemetryActivityEventEnricher<TelemetrySessionInfoEnricher> |
||||
|
{ |
||||
|
private readonly ITelemetryActivityStorage _telemetryActivityStorage; |
||||
|
|
||||
|
public TelemetryApplicationInfoEnricher(ITelemetryActivityStorage telemetryActivityStorage, IServiceProvider serviceProvider) : base(serviceProvider) |
||||
|
{ |
||||
|
_telemetryActivityStorage = telemetryActivityStorage; |
||||
|
} |
||||
|
|
||||
|
protected override Task<bool> CanExecuteAsync(ActivityContext context) |
||||
|
{ |
||||
|
return Task.FromResult(context.SessionType == SessionType.ApplicationRuntime); |
||||
|
} |
||||
|
|
||||
|
protected override Task ExecuteAsync(ActivityContext context) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var entryAssembly = Assembly.GetEntryAssembly(); |
||||
|
if (entryAssembly is null) |
||||
|
{ |
||||
|
context.Terminate(); |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
var projectMetaData = AbpProjectMetadataReader.ReadProjectMetadata(entryAssembly); |
||||
|
if (projectMetaData?.ProjectId == null || projectMetaData.AbpSlnPath.IsNullOrEmpty()) |
||||
|
{ |
||||
|
context.Terminate(); |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
if (!_telemetryActivityStorage.ShouldAddProjectInfo(projectMetaData.ProjectId.Value)) |
||||
|
{ |
||||
|
IgnoreChildren = true; |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
var solutionId = ReadSolutionIdFromSolutionPath(projectMetaData.AbpSlnPath); |
||||
|
|
||||
|
if (!solutionId.HasValue) |
||||
|
{ |
||||
|
IgnoreChildren = true; |
||||
|
context.Terminate(); |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
context.ExtraProperties[ActivityPropertyNames.SolutionPath] = projectMetaData.AbpSlnPath; |
||||
|
context.Current[ActivityPropertyNames.ProjectType] = projectMetaData.Role ?? string.Empty; |
||||
|
context.Current[ActivityPropertyNames.ProjectId] = projectMetaData.ProjectId.Value; |
||||
|
context.Current[ActivityPropertyNames.SolutionId] = solutionId; |
||||
|
context.Current[ActivityPropertyNames.HasProjectInfo] = true; |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
//ignored
|
||||
|
} |
||||
|
|
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
private static Guid? ReadSolutionIdFromSolutionPath(string solutionPath) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
if (solutionPath.IsNullOrEmpty()) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
using var fs = new FileStream(solutionPath!, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); |
||||
|
using var doc = JsonDocument.Parse(fs, new JsonDocumentOptions |
||||
|
{ |
||||
|
AllowTrailingCommas = true |
||||
|
}); |
||||
|
if (doc.RootElement.TryGetProperty("id", out var property) && property.TryGetGuid(out var solutionId)) |
||||
|
{ |
||||
|
return solutionId; |
||||
|
} |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
// ignored
|
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,79 @@ |
|||||
|
using System; |
||||
|
using System.Globalization; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Internal.Telemetry.Activity.Contracts; |
||||
|
using Volo.Abp.Internal.Telemetry.Constants; |
||||
|
using Volo.Abp.Internal.Telemetry.Constants.Enums; |
||||
|
using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.Activity.Providers; |
||||
|
|
||||
|
[ExposeServices(typeof(ITelemetryActivityEventEnricher))] |
||||
|
internal sealed class TelemetryDeviceInfoEnricher : TelemetryActivityEventEnricher |
||||
|
{ |
||||
|
private readonly ITelemetryActivityStorage _telemetryActivityStorage; |
||||
|
private readonly ISoftwareInfoProvider _softwareInfoProvider; |
||||
|
|
||||
|
public TelemetryDeviceInfoEnricher(ITelemetryActivityStorage telemetryActivityStorage, |
||||
|
ISoftwareInfoProvider softwareInfoProvider, IServiceProvider serviceProvider) : base(serviceProvider) |
||||
|
{ |
||||
|
_telemetryActivityStorage = telemetryActivityStorage; |
||||
|
_softwareInfoProvider = softwareInfoProvider; |
||||
|
} |
||||
|
protected async override Task ExecuteAsync(ActivityContext context) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var deviceId = DeviceManager.GetUniquePhysicalKey(true); |
||||
|
context.Current[ActivityPropertyNames.DeviceId] = deviceId; |
||||
|
|
||||
|
if (!_telemetryActivityStorage.ShouldAddDeviceInfo()) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var softwareList = await _softwareInfoProvider.GetSoftwareInfoAsync(); |
||||
|
|
||||
|
context.Current[ActivityPropertyNames.InstalledSoftwares] = softwareList; |
||||
|
context.Current[ActivityPropertyNames.DeviceLanguage] = CultureInfo.CurrentUICulture.Name; |
||||
|
context.Current[ActivityPropertyNames.OperatingSystem] = GetOperatingSystem(); |
||||
|
context.Current[ActivityPropertyNames.CountryIsoCode] = GetCountry(); |
||||
|
context.Current[ActivityPropertyNames.HasDeviceInfo] = true; |
||||
|
context.Current[ActivityPropertyNames.OperatingSystemArchitecture] = RuntimeInformation.OSArchitecture.ToString(); |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
//ignored
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static OperationSystem GetOperatingSystem() |
||||
|
{ |
||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) |
||||
|
{ |
||||
|
return OperationSystem.Windows; |
||||
|
} |
||||
|
|
||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) |
||||
|
{ |
||||
|
return OperationSystem.Linux; |
||||
|
} |
||||
|
|
||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) |
||||
|
{ |
||||
|
return OperationSystem.MacOS; |
||||
|
} |
||||
|
|
||||
|
return OperationSystem.Unknown; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
private static string GetCountry() |
||||
|
{ |
||||
|
var region = new RegionInfo(CultureInfo.InstalledUICulture.Name); |
||||
|
return region.TwoLetterISORegionName; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Internal.Telemetry.Activity.Contracts; |
||||
|
using Volo.Abp.Internal.Telemetry.Constants; |
||||
|
using Volo.Abp.Internal.Telemetry.Constants.Enums; |
||||
|
using Volo.Abp.Modularity; |
||||
|
using Volo.Abp.Reflection; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.Activity.Providers; |
||||
|
|
||||
|
[ExposeServices(typeof(ITelemetryActivityEventEnricher), typeof(IHasParentTelemetryActivityEventEnricher<TelemetryApplicationInfoEnricher>))] |
||||
|
internal sealed class TelemetryModuleInfoEnricher : TelemetryActivityEventEnricher, IHasParentTelemetryActivityEventEnricher<TelemetryApplicationInfoEnricher> |
||||
|
{ |
||||
|
private readonly IModuleContainer _moduleContainer; |
||||
|
private readonly IAssemblyFinder _assemblyFinder; |
||||
|
|
||||
|
public TelemetryModuleInfoEnricher(IModuleContainer moduleContainer, IAssemblyFinder assemblyFinder, |
||||
|
IServiceProvider serviceProvider) : base(serviceProvider) |
||||
|
{ |
||||
|
_moduleContainer = moduleContainer; |
||||
|
_assemblyFinder = assemblyFinder; |
||||
|
} |
||||
|
|
||||
|
protected override Task<bool> CanExecuteAsync(ActivityContext context) |
||||
|
{ |
||||
|
return Task.FromResult(context.SessionType == SessionType.ApplicationRuntime); |
||||
|
} |
||||
|
|
||||
|
protected override Task ExecuteAsync(ActivityContext context) |
||||
|
{ |
||||
|
context.Current[ActivityPropertyNames.ModuleCount] = _moduleContainer.Modules.Count; |
||||
|
context.Current[ActivityPropertyNames.ProjectCount] = _assemblyFinder.Assemblies.Count(x => |
||||
|
!x.FullName.IsNullOrEmpty() && |
||||
|
!x.FullName.StartsWith(TelemetryConsts.VoloNameSpaceFilter)); |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Internal.Telemetry.Activity.Contracts; |
||||
|
using Volo.Abp.Internal.Telemetry.Constants; |
||||
|
using Volo.Abp.Internal.Telemetry.Constants.Enums; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.Activity.Providers; |
||||
|
[ExposeServices(typeof(ITelemetryActivityEventEnricher))] |
||||
|
public class TelemetrySessionInfoEnricher : TelemetryActivityEventEnricher |
||||
|
{ |
||||
|
public override int ExecutionOrder { get; set; } = 10; |
||||
|
|
||||
|
public TelemetrySessionInfoEnricher(IServiceProvider serviceProvider) : base(serviceProvider) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override Task ExecuteAsync(ActivityContext context) |
||||
|
{ |
||||
|
context.Current[ActivityPropertyNames.SessionType] = SessionType.ApplicationRuntime; |
||||
|
context.Current[ActivityPropertyNames.SessionId] = Guid.NewGuid().ToString(); |
||||
|
context.Current[ActivityPropertyNames.IsFirstSession] = !File.Exists(TelemetryPaths.ActivityStorage); |
||||
|
|
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,130 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.IO; |
||||
|
using System.Text.Json; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Internal.Telemetry.Activity.Contracts; |
||||
|
using Volo.Abp.Internal.Telemetry.Constants; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.Activity.Providers; |
||||
|
|
||||
|
[ExposeServices(typeof(ITelemetryActivityEventEnricher), typeof(IHasParentTelemetryActivityEventEnricher<TelemetrySessionInfoEnricher>))] |
||||
|
internal sealed class TelemetrySolutionInfoEnricher : TelemetryActivityEventEnricher, IHasParentTelemetryActivityEventEnricher<TelemetrySessionInfoEnricher> |
||||
|
{ |
||||
|
private readonly ITelemetryActivityStorage _telemetryActivityStorage; |
||||
|
|
||||
|
public TelemetrySolutionInfoEnricher(ITelemetryActivityStorage telemetryActivityStorage, IServiceProvider serviceProvider) : base(serviceProvider) |
||||
|
{ |
||||
|
_telemetryActivityStorage = telemetryActivityStorage; |
||||
|
} |
||||
|
|
||||
|
protected override Task<bool> CanExecuteAsync(ActivityContext context) |
||||
|
{ |
||||
|
if (context.SolutionId.HasValue && !context.SolutionPath.IsNullOrEmpty()) |
||||
|
{ |
||||
|
return Task.FromResult(_telemetryActivityStorage.ShouldAddSolutionInformation(context.SolutionId.Value)); |
||||
|
} |
||||
|
|
||||
|
return Task.FromResult(false); |
||||
|
} |
||||
|
|
||||
|
protected override Task ExecuteAsync(ActivityContext context) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var jsonContent = File.ReadAllText(context.SolutionPath!); |
||||
|
using var doc = JsonDocument.Parse(jsonContent, new JsonDocumentOptions |
||||
|
{ |
||||
|
AllowTrailingCommas = true |
||||
|
}); |
||||
|
|
||||
|
var root = doc.RootElement; |
||||
|
|
||||
|
if (root.TryGetProperty("creatingStudioConfiguration", out var creatingStudioConfiguration)) |
||||
|
{ |
||||
|
AddSolutionCreationConfiguration(context, creatingStudioConfiguration); |
||||
|
} |
||||
|
|
||||
|
if (root.TryGetProperty("modules", out var modulesElement)) |
||||
|
{ |
||||
|
AddModuleInfo(context, modulesElement); |
||||
|
} |
||||
|
|
||||
|
context.Current[ActivityPropertyNames.HasSolutionInfo] = true; |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
//ignored
|
||||
|
} |
||||
|
|
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
private static void AddSolutionCreationConfiguration(ActivityContext context, JsonElement config) |
||||
|
{ |
||||
|
context.Current[ActivityPropertyNames.Template] = TelemetryJsonExtensions.GetStringOrNull(config, "template"); |
||||
|
context.Current[ActivityPropertyNames.CreatedAbpStudioVersion] = TelemetryJsonExtensions.GetStringOrNull(config, "createdAbpStudioVersion"); |
||||
|
context.Current[ActivityPropertyNames.MultiTenancy] = TelemetryJsonExtensions.GetBooleanOrNull(config, "multiTenancy"); |
||||
|
context.Current[ActivityPropertyNames.UiFramework] = TelemetryJsonExtensions.GetStringOrNull(config, "uiFramework"); |
||||
|
context.Current[ActivityPropertyNames.DatabaseProvider] = TelemetryJsonExtensions.GetStringOrNull(config, "databaseProvider"); |
||||
|
context.Current[ActivityPropertyNames.Theme] = TelemetryJsonExtensions.GetStringOrNull(config, "theme"); |
||||
|
context.Current[ActivityPropertyNames.ThemeStyle] = TelemetryJsonExtensions.GetStringOrNull(config, "themeStyle"); |
||||
|
context.Current[ActivityPropertyNames.HasPublicWebsite] = TelemetryJsonExtensions.GetBooleanOrNull(config, "publicWebsite"); |
||||
|
context.Current[ActivityPropertyNames.IsTiered] = TelemetryJsonExtensions.GetBooleanOrNull(config, "tiered"); |
||||
|
context.Current[ActivityPropertyNames.SocialLogins] = TelemetryJsonExtensions.GetBooleanOrNull(config, "socialLogin"); |
||||
|
context.Current[ActivityPropertyNames.DatabaseManagementSystem] = TelemetryJsonExtensions.GetStringOrNull(config, "databaseManagementSystem"); |
||||
|
context.Current[ActivityPropertyNames.IsSeparateTenantSchema] = TelemetryJsonExtensions.GetBooleanOrNull(config, "separateTenantSchema"); |
||||
|
context.Current[ActivityPropertyNames.MobileFramework] = TelemetryJsonExtensions.GetStringOrNull(config, "mobileFramework"); |
||||
|
context.Current[ActivityPropertyNames.IncludeTests] = TelemetryJsonExtensions.GetBooleanOrNull(config, "includeTests"); |
||||
|
context.Current[ActivityPropertyNames.DynamicLocalization] = TelemetryJsonExtensions.GetBooleanOrNull(config, "dynamicLocalization"); |
||||
|
context.Current[ActivityPropertyNames.KubernetesConfiguration] = TelemetryJsonExtensions.GetBooleanOrNull(config, "kubernetesConfiguration"); |
||||
|
context.Current[ActivityPropertyNames.GrafanaDashboard] = TelemetryJsonExtensions.GetBooleanOrNull(config, "grafanaDashboard"); |
||||
|
} |
||||
|
|
||||
|
private static void AddModuleInfo(ActivityContext context, JsonElement modulesElement) |
||||
|
{ |
||||
|
var modules = new List<Dictionary<string, object?>>(); |
||||
|
|
||||
|
foreach (var module in modulesElement.EnumerateObject()) |
||||
|
{ |
||||
|
var modulePath = GetModuleFilePath(context.SolutionPath!, module); |
||||
|
if (modulePath.IsNullOrEmpty()) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
var moduleJsonFileContent = File.ReadAllText(modulePath); |
||||
|
using var moduleDoc = JsonDocument.Parse(moduleJsonFileContent); |
||||
|
|
||||
|
if (!moduleDoc.RootElement.TryGetProperty("imports", out var imports)) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
foreach (var import in imports.EnumerateObject()) |
||||
|
{ |
||||
|
modules.Add(new Dictionary<string, object?> |
||||
|
{ |
||||
|
{ ActivityPropertyNames.ModuleName, import.Name }, |
||||
|
{ ActivityPropertyNames.ModuleVersion, TelemetryJsonExtensions.GetStringOrNull(import.Value, "version") }, |
||||
|
{ ActivityPropertyNames.ModuleInstallationTime, TelemetryJsonExtensions.GetDateTimeOffsetOrNull(import.Value, "creationTime") } |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
context.Current[ActivityPropertyNames.InstalledModules] = modules; |
||||
|
} |
||||
|
|
||||
|
private static string? GetModuleFilePath(string solutionPath, JsonProperty module) |
||||
|
{ |
||||
|
var path = TelemetryJsonExtensions.GetStringOrNull(module.Value, "path"); |
||||
|
if (path.IsNullOrEmpty()) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
var fullPath = Path.Combine(Path.GetDirectoryName(solutionPath)!, path); |
||||
|
return File.Exists(fullPath) ? fullPath : null; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.Activity.Storage; |
||||
|
|
||||
|
internal class FailedActivityInfo |
||||
|
{ |
||||
|
public DateTimeOffset FirstFailTime { get; set; } |
||||
|
public DateTimeOffset LastFailTime { get; set; } |
||||
|
public int RetryCount { get; set; } |
||||
|
|
||||
|
public bool IsExpired() |
||||
|
{ |
||||
|
var now = DateTimeOffset.UtcNow; |
||||
|
|
||||
|
return RetryCount >= TelemetryPeriod.MaxActivityRetryCount || |
||||
|
now - FirstFailTime > TelemetryPeriod.MaxFailedActivityAge; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,207 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using System.Text; |
||||
|
using System.Text.Encodings.Web; |
||||
|
using System.Text.Json; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Internal.Telemetry.Activity.Contracts; |
||||
|
using Volo.Abp.Internal.Telemetry.Constants; |
||||
|
using Volo.Abp.Internal.Telemetry.Helpers; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.Activity.Storage; |
||||
|
|
||||
|
public class TelemetryActivityStorage : ITelemetryActivityStorage, ISingletonDependency |
||||
|
{ |
||||
|
private TelemetryActivityStorageState State { get; } |
||||
|
private readonly static JsonSerializerOptions JsonSerializerOptions = new() |
||||
|
{ |
||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, |
||||
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping |
||||
|
}; |
||||
|
|
||||
|
public TelemetryActivityStorage() |
||||
|
{ |
||||
|
CreateDirectoryIfNotExist(); |
||||
|
|
||||
|
State = LoadState(); |
||||
|
} |
||||
|
|
||||
|
public void SaveActivity(ActivityEvent activityEvent) |
||||
|
{ |
||||
|
State.Activities.Add(activityEvent); |
||||
|
|
||||
|
var activityName = activityEvent.Get<string>(ActivityPropertyNames.ActivityName); |
||||
|
|
||||
|
if (activityName == ActivityNameConsts.AbpStudioClose) |
||||
|
{ |
||||
|
State.SessionId = null; |
||||
|
} |
||||
|
|
||||
|
if (activityEvent.HasDeviceInfo()) |
||||
|
{ |
||||
|
State.LastDeviceInfoAddTime = DateTimeOffset.UtcNow; |
||||
|
} |
||||
|
|
||||
|
if (activityEvent.HasSolutionInfo()) |
||||
|
{ |
||||
|
var solutionId = activityEvent.Get<Guid>(ActivityPropertyNames.SolutionId); |
||||
|
State.Solutions[solutionId] = DateTimeOffset.UtcNow; |
||||
|
} |
||||
|
|
||||
|
if (activityEvent.HasProjectInfo()) |
||||
|
{ |
||||
|
var projectId = activityEvent.Get<Guid>(ActivityPropertyNames.ProjectId); |
||||
|
State.Projects[projectId] = DateTimeOffset.UtcNow; |
||||
|
} |
||||
|
|
||||
|
SaveState(); |
||||
|
} |
||||
|
|
||||
|
public List<ActivityEvent> GetActivities() |
||||
|
{ |
||||
|
return State.Activities; |
||||
|
} |
||||
|
|
||||
|
public Guid InitializeOrGetSession() |
||||
|
{ |
||||
|
if (State.SessionId.HasValue) |
||||
|
{ |
||||
|
return State.SessionId.Value; |
||||
|
} |
||||
|
|
||||
|
State.SessionId = Guid.NewGuid(); |
||||
|
SaveState(); |
||||
|
|
||||
|
return State.SessionId.Value; |
||||
|
} |
||||
|
|
||||
|
public void DeleteActivities(ActivityEvent[] activities) |
||||
|
{ |
||||
|
var activityIds = new HashSet<Guid>(activities.Select(x => x.Get<Guid>(ActivityPropertyNames.Id))); |
||||
|
|
||||
|
State.Activities.RemoveAll(x => activityIds.Contains(x.Get<Guid>(ActivityPropertyNames.Id))); |
||||
|
|
||||
|
SaveState(); |
||||
|
} |
||||
|
|
||||
|
public void MarkActivitiesAsFailed(ActivityEvent[] activities) |
||||
|
{ |
||||
|
var now = DateTimeOffset.UtcNow; |
||||
|
|
||||
|
foreach (var activity in activities) |
||||
|
{ |
||||
|
var activityId = activity.Get<Guid>(ActivityPropertyNames.Id); |
||||
|
|
||||
|
if (State.FailedActivities.TryGetValue(activityId, out var failedActivityInfo)) |
||||
|
{ |
||||
|
failedActivityInfo.RetryCount++; |
||||
|
failedActivityInfo.LastFailTime = now; |
||||
|
|
||||
|
if (!failedActivityInfo.IsExpired()) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
State.Activities.RemoveAll(x=> x.Get<Guid>(ActivityPropertyNames.Id) == activityId); |
||||
|
State.FailedActivities.Remove(activityId); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
State.FailedActivities[activityId] = new FailedActivityInfo |
||||
|
{ |
||||
|
FirstFailTime = now, |
||||
|
LastFailTime = now, |
||||
|
RetryCount = 1 |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
SaveState(); |
||||
|
} |
||||
|
|
||||
|
public bool ShouldAddDeviceInfo() |
||||
|
{ |
||||
|
return State.LastDeviceInfoAddTime is null || |
||||
|
DateTimeOffset.UtcNow - State.LastDeviceInfoAddTime > TelemetryPeriod.InformationSendPeriod; |
||||
|
} |
||||
|
|
||||
|
public bool ShouldAddSolutionInformation(Guid solutionId) |
||||
|
{ |
||||
|
return !State.Solutions.TryGetValue(solutionId, out var lastSend) || |
||||
|
DateTimeOffset.UtcNow - lastSend > TelemetryPeriod.InformationSendPeriod; |
||||
|
} |
||||
|
|
||||
|
public bool ShouldAddProjectInfo(Guid projectId) |
||||
|
{ |
||||
|
return !State.Projects.TryGetValue(projectId, out var lastSend) || |
||||
|
DateTimeOffset.UtcNow - lastSend > TelemetryPeriod.InformationSendPeriod; |
||||
|
} |
||||
|
|
||||
|
public bool ShouldSendActivities() |
||||
|
{ |
||||
|
return State.ActivitySendTime is null || |
||||
|
DateTimeOffset.UtcNow - State.ActivitySendTime > TelemetryPeriod.ActivitySendPeriod; |
||||
|
} |
||||
|
|
||||
|
private void SaveState() |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var json = JsonSerializer.Serialize(State, JsonSerializerOptions); |
||||
|
var encryptedJson = Cryptography.Encrypt(json); |
||||
|
File.WriteAllText(TelemetryPaths.ActivityStorage, encryptedJson, Encoding.UTF8); |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
// Ignored
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static TelemetryActivityStorageState LoadState() |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
if (!File.Exists(TelemetryPaths.ActivityStorage)) |
||||
|
{ |
||||
|
return new TelemetryActivityStorageState(); |
||||
|
} |
||||
|
|
||||
|
var fileContent = MutexExecutor.ReadFileSafely(TelemetryPaths.ActivityStorage); |
||||
|
|
||||
|
if (fileContent.IsNullOrEmpty()) |
||||
|
{ |
||||
|
return new TelemetryActivityStorageState(); |
||||
|
} |
||||
|
|
||||
|
var json = Cryptography.Decrypt(fileContent); |
||||
|
|
||||
|
var state = JsonSerializer.Deserialize<TelemetryActivityStorageState>(json, JsonSerializerOptions)!; |
||||
|
state.Activities = state.Activities.Where(x => x != null).ToList(); |
||||
|
return state; |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
return new TelemetryActivityStorageState(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static void CreateDirectoryIfNotExist() |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var storageDirectory = Path.GetDirectoryName(TelemetryPaths.ActivityStorage)!; |
||||
|
|
||||
|
if (!Directory.Exists(storageDirectory)) |
||||
|
{ |
||||
|
Directory.CreateDirectory(storageDirectory); |
||||
|
} |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
// Ignored
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.Activity.Storage; |
||||
|
|
||||
|
internal class TelemetryActivityStorageState |
||||
|
{ |
||||
|
public DateTimeOffset? ActivitySendTime { get; set; } |
||||
|
public DateTimeOffset? LastDeviceInfoAddTime { get; set; } |
||||
|
public Guid? SessionId { get; set; } |
||||
|
public List<ActivityEvent> Activities { get; set; } = new(); |
||||
|
public Dictionary<Guid,DateTimeOffset> Solutions { get; set; } = new(); |
||||
|
public Dictionary<Guid, DateTimeOffset> Projects { get; set; } = new(); |
||||
|
public Dictionary<Guid, FailedActivityInfo> FailedActivities { get; set; } = new(); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.Activity.Storage; |
||||
|
|
||||
|
static internal class TelemetryPeriod |
||||
|
{ |
||||
|
private const string TestModeEnvironmentVariable = "ABP_TELEMETRY_TEST_MODE"; |
||||
|
|
||||
|
static TelemetryPeriod() |
||||
|
{ |
||||
|
var isTestMode = IsTestModeEnabled(); |
||||
|
|
||||
|
InformationSendPeriod = isTestMode |
||||
|
? TimeSpan.FromSeconds(15) |
||||
|
: TimeSpan.FromDays(7); |
||||
|
|
||||
|
ActivitySendPeriod = isTestMode |
||||
|
? TimeSpan.FromSeconds(5) |
||||
|
: TimeSpan.FromDays(1); |
||||
|
} |
||||
|
|
||||
|
public static TimeSpan ActivitySendPeriod { get; } |
||||
|
public static TimeSpan InformationSendPeriod { get; } |
||||
|
|
||||
|
public static int MaxActivityRetryCount { get; set; } = 3; |
||||
|
public static TimeSpan MaxFailedActivityAge { get; set; } = TimeSpan.FromDays(30); |
||||
|
|
||||
|
private static bool IsTestModeEnabled() |
||||
|
{ |
||||
|
var testModeVariable = |
||||
|
Environment.GetEnvironmentVariable(TestModeEnvironmentVariable, EnvironmentVariableTarget.User); |
||||
|
return bool.TryParse(testModeVariable, out var isTestMode) && isTestMode; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
using System; |
||||
|
using System.Text.Json; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.Activity; |
||||
|
|
||||
|
static internal class TelemetryJsonExtensions |
||||
|
{ |
||||
|
static internal string? GetStringOrNull(JsonElement element, string propertyName) |
||||
|
{ |
||||
|
return element.TryGetProperty(propertyName, out var property) |
||||
|
? property.GetString() ?? null |
||||
|
: null; |
||||
|
} |
||||
|
|
||||
|
static internal bool? GetBooleanOrNull(JsonElement element, string propertyName) |
||||
|
{ |
||||
|
if (element.TryGetProperty(propertyName, out var property) && bool.TryParse(property.GetString(), out var boolValue)) |
||||
|
{ |
||||
|
return boolValue; |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
static internal DateTimeOffset? GetDateTimeOffsetOrNull(JsonElement element, string propertyName) |
||||
|
{ |
||||
|
if (element.TryGetProperty(propertyName, out var date) && DateTimeOffset.TryParse(date.GetString(), out var dateTimeValue)) |
||||
|
{ |
||||
|
return dateTimeValue; |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
namespace Volo.Abp.Internal.Telemetry.Constants; |
||||
|
|
||||
|
public static class AbpPlatformUrls |
||||
|
{ |
||||
|
#if DEBUG
|
||||
|
public const string AbpTelemetryApiUrl = "https://localhost:44393/"; |
||||
|
#else
|
||||
|
public const string AbpTelemetryApiUrl = "https://telemetry.abp.io/"; |
||||
|
#endif
|
||||
|
|
||||
|
|
||||
|
} |
||||
@ -0,0 +1,76 @@ |
|||||
|
namespace Volo.Abp.Internal.Telemetry.Constants; |
||||
|
|
||||
|
public static class ActivityNameConsts |
||||
|
{ |
||||
|
public const string AbpStudioOpen = "AbpStudio.Open"; |
||||
|
public const string AbpStudioOpenFirstTimeForDevice = "AbpStudio.Open.FirstTimeForDevice"; |
||||
|
public const string AbpStudioOpenFirstTimeForUser = "AbpStudio.Open.FirstTimeForUser"; |
||||
|
public const string AbpStudioClose = "AbpStudio.Close"; |
||||
|
public const string AbpStudioCloseWithoutLogin = "AbpStudio.Close.WithoutLogin"; |
||||
|
public const string AbpStudioLogin = "AbpStudio.Login"; |
||||
|
public const string AbpStudioCampaignClick = "AbpStudio.Campaing.Click"; |
||||
|
public const string AbpStudioCommunityPostClick = "AbpStudio.CommunityPost.Click"; |
||||
|
public const string AbpStudioSolutionInitExisting = "AbpStudio.Solution.InitExisting"; |
||||
|
public const string AbpStudioSolutionNew = "AbpStudio.Solution.New"; |
||||
|
public const string AbpStudioSolutionNewMicroservice = "AbpStudio.Solution.New.Microservice"; |
||||
|
public const string AbpStudioSolutionAddModule = "AbpStudio.Solution.Add.Module"; |
||||
|
public const string AbpStudioSolutionAddModuleEmpty = "AbpStudio.Solution.Add.Module.Empty"; |
||||
|
public const string AbpStudioSolutionAddModuleDdd = "AbpStudio.Solution.Add.Module.Ddd"; |
||||
|
public const string AbpStudioSolutionAddModuleStandard = "AbpStudio.Solution.Add.Module.Standard"; |
||||
|
public const string AbpStudioSolutionAddMicroservice = "AbpStudio.Solution.Add.Microservice"; |
||||
|
public const string AbpStudioSolutionAddGateway = "AbpStudio.Solution.Add.Gateway"; |
||||
|
public const string AbpStudioSolutionAddWeb = "AbpStudio.Solution.Add.Web"; |
||||
|
public const string AbpStudioSolutionAddPackage = "AbpStudio.Solution.Add.Package"; |
||||
|
public const string AbpStudioSolutionAddPackageHttpApiLayer = "AbpStudio.Solution.Add.Package.HttpApiLayer"; |
||||
|
public const string AbpStudioAbpCliInstallLibs = "AbpStudio.AbpCli.InstallLibs"; |
||||
|
public const string AbpStudioAbpCliUpgradeAbp = "AbpStudio.AbpCli.UpgradeAbp"; |
||||
|
public const string AbpStudioAbpCliSwitchToStable = "AbpStudio.AbpCli.SwitchToStable"; |
||||
|
public const string AbpStudioAbpCliSwitchToPreview = "AbpStudio.AbpCli.SwitchToPreview"; |
||||
|
public const string AbpStudioAbpCliSwitchToNightly = "AbpStudio.AbpCli.SwitchToNightly"; |
||||
|
public const string AbpStudioAbpCliClean = "AbpStudio.AbpCli.Clean"; |
||||
|
public const string AbpStudioDotnetCliBuild = "AbpStudio.DotnetCli.Build"; |
||||
|
public const string AbpStudioDotnetCliGraphBuild = "AbpStudio.DotnetCli.GraphBuild"; |
||||
|
public const string AbpStudioDotnetCliClean = "AbpStudio.DotnetCli.Clean"; |
||||
|
public const string AbpStudioDotnetCliRestore = "AbpStudio.DotnetCli.Restore"; |
||||
|
public const string AbpStudioSolutionRunnerRunApp = "AbpStudio.SolutionRunner.RunApp"; |
||||
|
public const string AbpStudioSolutionRunnerAddCsharpApp = "AbpStudio.SolutionRunner.Add.CsharpApp"; |
||||
|
public const string AbpStudioSolutionRunnerAddCliApp = "AbpStudio.SolutionRunner.Add.CliApp"; |
||||
|
public const string AbpStudioSolutionRunnerAddProfile = "AbpStudio.SolutionRunner.Add.Profile"; |
||||
|
public const string AbpStudioSolutionRunnerAppManageMetadata = "AbpStudio.SolutionRunner.App.Manage.Metadata"; |
||||
|
public const string AbpStudioSolutionRunnerAppManageSecrets = "AbpStudio.SolutionRunner.App.Manage.Secrets"; |
||||
|
public const string AbpStudioSolutionOpen = "AbpStudio.Solution.Open"; |
||||
|
public const string AbpStudioMonitoringBrowse = "AbpStudio.Monitoring.Browse"; |
||||
|
public const string AbpStudioMonitoringHttpRequests = "AbpStudio.Monitoring.HttpRequests"; |
||||
|
public const string AbpStudioMonitoringHttpRequestsDetail = "AbpStudio.Monitoring.HttpRequests.Detail"; |
||||
|
public const string AbpStudioMonitoringEvents = "AbpStudio.Monitoring.Events"; |
||||
|
public const string AbpStudioMonitoringEventsDetail = "AbpStudio.Monitoring.Events.Detail"; |
||||
|
public const string AbpStudioMonitoringExceptions = "AbpStudio.Monitoring.Exceptions"; |
||||
|
public const string AbpStudioMonitoringExceptionsDetail = "AbpStudio.Monitoring.Exceptions.Detail"; |
||||
|
public const string AbpStudioMonitoringLogs = "AbpStudio.Monitoring.Logs"; |
||||
|
public const string AbpStudioKubernetesAddProfile = "AbpStudio.Kubernetes.Add.Profile"; |
||||
|
public const string AbpStudioKubernetesConnect = "AbpStudio.Kubernetes.Connect"; |
||||
|
public const string AbpStudioKubernetesIntercept = "AbpStudio.Kubernetes.Intercept"; |
||||
|
public const string AbpStudioKubernetesHelmCommandsInstall = "AbpStudio.Kubernetes.Helm.Commands.Install"; |
||||
|
public const string AbpStudioKubernetesHelmCommandsBuildImages = "AbpStudio.Kubernetes.Helm.Commands.BuildImages"; |
||||
|
public const string AbpStudioKubernetesHelmChartsRefreshSubCharts = "AbpStudio.Kubernetes.Helm.Charts.RefreshSubCharts"; |
||||
|
public const string AbpStudioKubernetesHelmChartsManageMetadata = "AbpStudio.Kubernetes.Helm.Charts.Manage.Metadata"; |
||||
|
public const string AbpStudioLogsShow = "AbpStudio.Logs.Show"; |
||||
|
public const string AbpStudioSuiteOpen = "AbpStudio.Suite.Open"; |
||||
|
public const string AbpStudioGlobalSecretsManage = "AbpStudio.GlobalSecrets.Manage"; |
||||
|
public const string AbpStudioGlobalMetadataManage = "AbpStudio.GlobalMetadata.Manage"; |
||||
|
public const string AbpCliCommandsNewSolution = "AbpCli.Comands.NewSolution"; |
||||
|
public const string AbpCliCommandsNewModule = "AbpCli.Comands.NewModule"; |
||||
|
public const string AbpCliCommandsNewPackage = "AbpCli.Comands.NewPackage"; |
||||
|
public const string AbpCliCommandsUpdate = "AbpCli.Comands.Update"; |
||||
|
public const string AbpCliCommandsClean = "AbpCli.Comands.Clean"; |
||||
|
public const string AbpCliCommandsAddPackage = "AbpCli.Comands.AddPackage"; |
||||
|
public const string AbpCliCommandsAddPackageRef = "AbpCli.Comands.AddPackageRef"; |
||||
|
public const string AbpCliCommandsInstallModule = "AbpCli.Comands.InstallModule"; |
||||
|
public const string AbpCliCommandsInstallLocalModule = "AbpCli.Comands.InstallLocalModule"; |
||||
|
public const string AbpCliCommandsListModules = "AbpCli.Comands.ListModules"; |
||||
|
public const string AbpCliRun = "AbpCli.Run"; |
||||
|
public const string AbpCliExit = "AbpCli.Exit"; |
||||
|
public const string ApplicationRun = "Application.Run"; |
||||
|
public const string AbpStudioBrowserOpen = "AbpStudio.Browser.Open"; |
||||
|
public const string Error = "Error"; |
||||
|
} |
||||
@ -0,0 +1,77 @@ |
|||||
|
namespace Volo.Abp.Internal.Telemetry.Constants; |
||||
|
|
||||
|
public static class ActivityPropertyNames |
||||
|
{ |
||||
|
public const string SessionId = "SessionId"; |
||||
|
public const string ActivityName = "ActivityName"; |
||||
|
public const string Error = "Error"; |
||||
|
public const string ErrorDetail = "ErrorDetail"; |
||||
|
public const string Id = "Id"; |
||||
|
public const string UserId = "UserId"; |
||||
|
public const string OrganizationId = "OrganizationId"; |
||||
|
public const string IpAddress = "IpAddress"; |
||||
|
public const string IsFirstSession = "IsFirstSession"; |
||||
|
public const string DeviceId = "DeviceId"; |
||||
|
public const string DeviceLanguage = "DeviceLanguage"; |
||||
|
public const string OperatingSystem = "OperatingSystem"; |
||||
|
public const string CountryIsoCode = "CountryIsoCode"; |
||||
|
public const string InstalledSoftwares = "InstalledSoftwares"; |
||||
|
public const string ControllerCount = "ControllerCount"; |
||||
|
public const string EntityCount = "EntityCount"; |
||||
|
public const string ProjectCount = "ProjectCount"; |
||||
|
public const string ModuleCount = "ModuleCount"; |
||||
|
public const string PermissionCount = "PermissionCount"; |
||||
|
public const string AppServiceCount = "AppServiceCount"; |
||||
|
public const string ProjectType = "ProjectType"; |
||||
|
public const string ProjectId = "ProjectId"; |
||||
|
public const string SolutionId = "SolutionId"; |
||||
|
public const string Template = "Template"; |
||||
|
public const string CreatedAbpStudioVersion = "CreatedAbpStudioVersion"; |
||||
|
public const string IsTiered = "IsTiered"; |
||||
|
public const string UiFramework = "UiFramework"; |
||||
|
public const string DatabaseProvider = "DatabaseProvider"; |
||||
|
public const string DatabaseManagementSystem = "DatabaseManagementSystem"; |
||||
|
public const string IsSeparateTenantSchema = "IsSeparateTenantSchema"; |
||||
|
public const string Theme = "Theme"; |
||||
|
public const string ThemeStyle = "ThemeStyle"; |
||||
|
public const string MobileFramework = "MobileFramework"; |
||||
|
public const string HasPublicWebsite = "HasPublicWebsite"; |
||||
|
public const string IncludeTests = "IncludeTests"; |
||||
|
public const string MultiTenancy = "MultiTenancy"; |
||||
|
public const string DynamicLocalization = "DynamicLocalization"; |
||||
|
public const string KubernetesConfiguration = "KubernetesConfiguration"; |
||||
|
public const string GrafanaDashboard = "GrafanaDashboard"; |
||||
|
public const string SocialLogins = "SocialLogins"; |
||||
|
public const string InstalledModules = "InstalledModules"; |
||||
|
public const string SolutionPath = "SolutionPath"; |
||||
|
public const string LicenseType = "LicenseType"; |
||||
|
public const string SessionType = "SessionType"; |
||||
|
public const string HasError = "HasError"; |
||||
|
public const string ActivityDuration = "ActivityDuration"; |
||||
|
public const string ActivityDetails = "ActivityDetails"; |
||||
|
public const string Time = "Time"; |
||||
|
public const string SoftwareName = "Name"; |
||||
|
public const string SoftwareVersion = "Version"; |
||||
|
public const string SoftwareUiTheme = "UiTheme"; |
||||
|
public const string SoftwareType = "SoftwareType"; |
||||
|
public const string WebFramework = "WebFramework"; |
||||
|
public const string Dbms = "Dbms"; |
||||
|
public const string UiTheme = "UiTheme"; |
||||
|
public const string UiThemeStyle = "UiThemeStyle"; |
||||
|
public const string MobileApp = "MobileApp"; |
||||
|
public const string SampleCrudPage = "SampleCrudPage"; |
||||
|
public const string FirstAbpVersion = "FirstAbpVersion"; |
||||
|
public const string FirstDotnetVersion = "FirstDotnetVersion"; |
||||
|
public const string CreationTool = "CreationTool"; |
||||
|
public const string ModuleName = "ModuleName"; |
||||
|
public const string ModuleVersion = "ModuleVersion"; |
||||
|
public const string ModuleInstallationTime = "ModuleInstallationTime"; |
||||
|
public const string ExtraProperties = "ExtraProperties"; |
||||
|
public const string HasSolutionInfo = "HasSolutionInfo"; |
||||
|
public const string HasDeviceInfo = "HasDeviceInfo"; |
||||
|
public const string HasProjectInfo = "HasProjectInfo"; |
||||
|
public const string ErrorMessage = "ErrorMessage"; |
||||
|
public const string FailingActivity = "FailingActivity"; |
||||
|
public const string OperatingSystemArchitecture = "OperatingSystemArchitecture"; |
||||
|
public const string AdditionalProperties = "AdditionalProperties"; |
||||
|
} |
||||
@ -0,0 +1,260 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.Constants; |
||||
|
|
||||
|
static internal class DeviceManager |
||||
|
{ |
||||
|
public static string GetUniquePhysicalKey(bool shouldHash) |
||||
|
{ |
||||
|
char platformId = '?'; |
||||
|
char osArchitecture = '?'; |
||||
|
string operatingSystem = "?"; |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
string osPrefix; |
||||
|
string uniqueKey; |
||||
|
|
||||
|
platformId = GetPlatformIdOrDefault(); |
||||
|
osArchitecture = GetOsArchitectureOrDefault(); |
||||
|
|
||||
|
if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform |
||||
|
.Windows)) |
||||
|
{ |
||||
|
operatingSystem = "Windows"; |
||||
|
uniqueKey = GetUniqueKeyForWindows(); |
||||
|
osPrefix = "W"; |
||||
|
} |
||||
|
else if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices |
||||
|
.OSPlatform.Linux)) |
||||
|
{ |
||||
|
operatingSystem = "Linux"; |
||||
|
uniqueKey = GetHarddiskSerialForLinux(); |
||||
|
osPrefix = "L"; |
||||
|
} |
||||
|
else if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices |
||||
|
.OSPlatform.OSX)) //MAC
|
||||
|
{ |
||||
|
operatingSystem = "OSX"; |
||||
|
uniqueKey = GetHarddiskSerialForOsX(); |
||||
|
osPrefix = "O"; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
operatingSystem = "Other"; |
||||
|
uniqueKey = GetNetworkAdapterSerial(); |
||||
|
osPrefix = "X"; |
||||
|
} |
||||
|
|
||||
|
if (shouldHash) |
||||
|
{ |
||||
|
uniqueKey = ConvertToMd5(uniqueKey).ToUpperInvariant(); |
||||
|
} |
||||
|
|
||||
|
return osPrefix + platformId + osArchitecture + "-" + uniqueKey; |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
return Guid.NewGuid().ToString(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static string GetNetworkAdapterSerial() |
||||
|
{ |
||||
|
string macAddress = string.Empty; |
||||
|
|
||||
|
var networkInterfaces = System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces(); |
||||
|
foreach (var networkInterface in networkInterfaces) |
||||
|
{ |
||||
|
if (networkInterface.NetworkInterfaceType == System.Net.NetworkInformation.NetworkInterfaceType.Loopback) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
var physicalAddress = networkInterface.GetPhysicalAddress().ToString(); |
||||
|
if (string.IsNullOrEmpty(physicalAddress)) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
macAddress = physicalAddress; |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
return macAddress!; |
||||
|
} |
||||
|
|
||||
|
private static char GetPlatformIdOrDefault(char defaultValue = '*') |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
return ((int)System.Environment.OSVersion.Platform).ToString()[0]; |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
return defaultValue; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static string ConvertToMd5(string text) |
||||
|
{ |
||||
|
using (var md5 = new System.Security.Cryptography.MD5CryptoServiceProvider()) |
||||
|
{ |
||||
|
return EncodeBase64(md5.ComputeHash(System.Text.Encoding.UTF8.GetBytes(text))); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static string EncodeBase64(byte[] ba) |
||||
|
{ |
||||
|
var hex = new System.Text.StringBuilder(ba.Length * 2); |
||||
|
|
||||
|
foreach (var b in ba) |
||||
|
{ |
||||
|
hex.AppendFormat("{0:x2}", b); |
||||
|
} |
||||
|
|
||||
|
return hex.ToString(); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
private static char GetOsArchitectureOrDefault(char defaultValue = '*') |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
return ((int)System.Runtime.InteropServices.RuntimeInformation.OSArchitecture).ToString()[0]; |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
return defaultValue; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static string GetUniqueKeyForWindows() |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
return GetProcessorIdForWindows(); |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
return GetWindowsMachineUniqueId(); |
||||
|
} |
||||
|
|
||||
|
private static string GetProcessorIdForWindows() |
||||
|
{ |
||||
|
using (var managementObjectSearcher = |
||||
|
new System.Management.ManagementObjectSearcher("SELECT ProcessorId FROM Win32_Processor")) |
||||
|
{ |
||||
|
using (var searcherObj = managementObjectSearcher.Get()) |
||||
|
{ |
||||
|
if (searcherObj.Count == 0) |
||||
|
{ |
||||
|
throw new System.Exception("No unique computer ID found for this computer!"); |
||||
|
} |
||||
|
|
||||
|
var managementObjectEnumerator = searcherObj.GetEnumerator(); |
||||
|
managementObjectEnumerator.MoveNext(); |
||||
|
return managementObjectEnumerator.Current.GetPropertyValue("ProcessorId").ToString()!; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static string GetWindowsMachineUniqueId() |
||||
|
{ |
||||
|
return RunCommandAndGetOutput("powershell (Get-CimInstance -Class Win32_ComputerSystemProduct).UUID"); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
private static string GetHarddiskSerialForLinux() |
||||
|
{ |
||||
|
return RunCommandAndGetOutput( |
||||
|
"udevadm info --query=all --name=/dev/sda | grep ID_SERIAL_SHORT | tr -d \"ID_SERIAL_SHORT=:\""); |
||||
|
} |
||||
|
|
||||
|
private static string GetHarddiskSerialForOsX() |
||||
|
{ |
||||
|
var command = |
||||
|
"ioreg -rd1 -c IOPlatformExpertDevice | awk '/IOPlatformUUID/ { split($0, line, \"\\\"\"); printf(\"%s\\n\", line[4]); }'"; |
||||
|
|
||||
|
command = System.Text.RegularExpressions.Regex.Replace(command, @"(\\*)" + "\"", @"$1$1\" + "\""); |
||||
|
|
||||
|
return RunCommandAndGetOutput(command); |
||||
|
} |
||||
|
|
||||
|
private static string RunCommandAndGetOutput(string command) |
||||
|
{ |
||||
|
var output = ""; |
||||
|
|
||||
|
using (var process = new System.Diagnostics.Process()) |
||||
|
{ |
||||
|
process.StartInfo = new System.Diagnostics.ProcessStartInfo(GetFileName()) |
||||
|
{ |
||||
|
Arguments = GetArguments(command), |
||||
|
UseShellExecute = false, |
||||
|
CreateNoWindow = true, |
||||
|
RedirectStandardOutput = true, |
||||
|
RedirectStandardError = true |
||||
|
}; |
||||
|
|
||||
|
process.Start(); |
||||
|
process?.WaitForExit(); |
||||
|
|
||||
|
using (var stdOut = process!.StandardOutput) |
||||
|
{ |
||||
|
using (var stdErr = process.StandardError) |
||||
|
{ |
||||
|
output = stdOut.ReadToEnd(); |
||||
|
output += stdErr.ReadToEnd(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return output.Trim(); |
||||
|
} |
||||
|
|
||||
|
private static string GetFileName() |
||||
|
{ |
||||
|
if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform( |
||||
|
System.Runtime.InteropServices.OSPlatform.OSX) || |
||||
|
System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform |
||||
|
.Linux)) |
||||
|
{ |
||||
|
string[] fileNames = { "/bin/bash", "/usr/bin/bash", "/bin/sh", "/usr/bin/sh" }; |
||||
|
foreach (var fileName in fileNames) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
if (System.IO.File.Exists(fileName)) |
||||
|
{ |
||||
|
return fileName; |
||||
|
} |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
//ignore
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return "/bin/bash"; |
||||
|
} |
||||
|
|
||||
|
//Windows default.
|
||||
|
return "cmd.exe"; |
||||
|
} |
||||
|
|
||||
|
private static string GetArguments(string command) |
||||
|
{ |
||||
|
if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform( |
||||
|
System.Runtime.InteropServices.OSPlatform.OSX) || |
||||
|
System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform |
||||
|
.Linux)) |
||||
|
{ |
||||
|
return "-c \"" + command + "\""; |
||||
|
} |
||||
|
|
||||
|
//Windows default.
|
||||
|
return "/C \"" + command + "\""; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
namespace Volo.Abp.Internal.Telemetry.Constants.Enums; |
||||
|
|
||||
|
public enum AbpTool : byte |
||||
|
{ |
||||
|
Unknown = 0, |
||||
|
StudioUI = 1, |
||||
|
StudioCli = 2, |
||||
|
OldCli = 3 |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
namespace Volo.Abp.Internal.Telemetry.Constants.Enums; |
||||
|
|
||||
|
public enum OperationSystem |
||||
|
{ |
||||
|
Unknown = 0, |
||||
|
Windows = 1, |
||||
|
MacOS = 2, |
||||
|
Linux = 3, |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
namespace Volo.Abp.Internal.Telemetry.Constants.Enums; |
||||
|
|
||||
|
public enum SessionType |
||||
|
{ |
||||
|
Unknown = 0, |
||||
|
AbpStudio = 1, |
||||
|
AbpCli = 2, |
||||
|
ApplicationRuntime = 3 |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
namespace Volo.Abp.Internal.Telemetry.Constants.Enums; |
||||
|
|
||||
|
public enum SoftwareType : byte |
||||
|
{ |
||||
|
Others = 0, |
||||
|
AbpStudio = 1, |
||||
|
DotnetSdk = 2, |
||||
|
OperatingSystem = 3, |
||||
|
Ide = 4, |
||||
|
Browser = 5 |
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
namespace Volo.Abp.Internal.Telemetry.Constants; |
||||
|
|
||||
|
public class TelemetryConsts |
||||
|
{ |
||||
|
public const string VoloNameSpaceFilter = "Volo."; |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
using System; |
||||
|
using System.IO; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.Constants; |
||||
|
|
||||
|
public static class TelemetryPaths |
||||
|
{ |
||||
|
public static string AccessToken => Path.Combine(AbpRootPath, "cli", "access-token.bin"); |
||||
|
public static string ComputerId => Path.Combine(AbpRootPath, "cli", "computer-id.bin"); |
||||
|
public static string ActivityStorage => Path.Combine(AbpRootPath , "telemetry", "activity-storage.bin"); |
||||
|
public static string Studio => Path.Combine(AbpRootPath, "studio"); |
||||
|
private readonly static string AbpRootPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".abp"); |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; |
||||
|
|
||||
|
internal interface ISoftwareDetector |
||||
|
{ |
||||
|
string Name { get; } |
||||
|
Task<SoftwareInfo?> DetectAsync(); |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; |
||||
|
|
||||
|
internal interface ISoftwareInfoProvider |
||||
|
{ |
||||
|
Task<List<SoftwareInfo>> GetSoftwareInfoAsync(); |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
using Volo.Abp.Internal.Telemetry.Constants.Enums; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; |
||||
|
|
||||
|
internal class SoftwareInfo(string name, string? version, string? uiTheme, SoftwareType softwareType) |
||||
|
{ |
||||
|
public string Name { get; set; } = name; |
||||
|
public string? Version { get; set; } = version; |
||||
|
public string? UiTheme { get; set; } = uiTheme; |
||||
|
public SoftwareType SoftwareType { get; set; } = softwareType; |
||||
|
} |
||||
@ -0,0 +1,78 @@ |
|||||
|
using System.Diagnostics; |
||||
|
using System.Text; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core; |
||||
|
|
||||
|
[ExposeServices(typeof(ISoftwareDetector))] |
||||
|
abstract internal class SoftwareDetector: ISoftwareDetector , ISingletonDependency |
||||
|
{ |
||||
|
public abstract string Name { get; } |
||||
|
public abstract Task<SoftwareInfo?> DetectAsync(); |
||||
|
|
||||
|
protected virtual async Task<string?> ExecuteCommandAsync(string command, string? arg) |
||||
|
{ |
||||
|
var outputBuilder = new StringBuilder(); |
||||
|
|
||||
|
var processStartInfo = new ProcessStartInfo |
||||
|
{ |
||||
|
FileName = command, |
||||
|
Arguments = arg ?? "", |
||||
|
RedirectStandardOutput = true, |
||||
|
RedirectStandardError = true, |
||||
|
UseShellExecute = false, |
||||
|
CreateNoWindow = true |
||||
|
}; |
||||
|
|
||||
|
using var process = new Process(); |
||||
|
process.StartInfo = processStartInfo; |
||||
|
process.EnableRaisingEvents = true; |
||||
|
|
||||
|
var tcs = new TaskCompletionSource<bool>(); |
||||
|
|
||||
|
process.OutputDataReceived += (sender, e) => |
||||
|
{ |
||||
|
if (e.Data != null) |
||||
|
{ |
||||
|
outputBuilder.AppendLine(e.Data); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
process.ErrorDataReceived += (sender, e) => |
||||
|
{ |
||||
|
if (e.Data != null) |
||||
|
{ |
||||
|
outputBuilder.AppendLine(e.Data); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
process.Exited += (sender, e) => |
||||
|
{ |
||||
|
tcs.TrySetResult(true); |
||||
|
}; |
||||
|
|
||||
|
process.Start(); |
||||
|
process.BeginOutputReadLine(); |
||||
|
process.BeginErrorReadLine(); |
||||
|
|
||||
|
await tcs.Task; |
||||
|
|
||||
|
var output = outputBuilder.ToString().Trim(); |
||||
|
return string.IsNullOrWhiteSpace(output) ? null : output; |
||||
|
} |
||||
|
|
||||
|
protected string? GetFileVersion(string filePath) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var versionInfo = FileVersionInfo.GetVersionInfo(filePath); |
||||
|
return versionInfo.FileVersion; |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
return string.Empty; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,76 @@ |
|||||
|
using System.IO; |
||||
|
using System.Text.Json; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Internal.Telemetry.Constants; |
||||
|
using Volo.Abp.Internal.Telemetry.Constants.Enums; |
||||
|
using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; |
||||
|
using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors; |
||||
|
|
||||
|
internal sealed class AbpStudioDetector : SoftwareDetector |
||||
|
{ |
||||
|
public override string Name => "Abp Studio"; |
||||
|
private const string AbpStudioVersionExtensionName = "Volo.Abp.Studio.Extensions.StandardSolutionTemplates"; |
||||
|
|
||||
|
public override Task<SoftwareInfo?> DetectAsync() |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var uiTheme = GetAbpStudioUiTheme(); |
||||
|
var version = GetAbpStudioVersion(); |
||||
|
|
||||
|
return Task.FromResult<SoftwareInfo?>(new SoftwareInfo(Name, version, uiTheme, SoftwareType.AbpStudio)); |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
return Task.FromResult<SoftwareInfo?>(null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private string? GetAbpStudioUiTheme() |
||||
|
{ |
||||
|
var ideStateJsonPath = Path.Combine( |
||||
|
TelemetryPaths.Studio, |
||||
|
"ui", |
||||
|
"ide-state.json" |
||||
|
); |
||||
|
if (!File.Exists(ideStateJsonPath)) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
using var fs = new FileStream(ideStateJsonPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); |
||||
|
using var doc = JsonDocument.Parse(fs); |
||||
|
|
||||
|
return doc.RootElement.TryGetProperty("theme", out var themeElement) ? themeElement.GetString() : null; |
||||
|
} |
||||
|
|
||||
|
private string? GetAbpStudioVersion() |
||||
|
{ |
||||
|
var extensionsFilePath = Path.Combine(TelemetryPaths.Studio, "extensions.json"); |
||||
|
|
||||
|
if (!File.Exists(extensionsFilePath)) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
using var fs = new FileStream(extensionsFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); |
||||
|
using var doc = JsonDocument.Parse(fs); |
||||
|
|
||||
|
if (doc.RootElement.TryGetProperty("Extensions", out var extensionsElement) && |
||||
|
extensionsElement.ValueKind == JsonValueKind.Array) |
||||
|
{ |
||||
|
foreach (var extension in extensionsElement.EnumerateArray()) |
||||
|
{ |
||||
|
if (extension.TryGetProperty("name", out var nameProp) && |
||||
|
nameProp.GetString() == AbpStudioVersionExtensionName && |
||||
|
extension.TryGetProperty("version", out var versionProp)) |
||||
|
{ |
||||
|
return versionProp.GetString(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,48 @@ |
|||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Internal.Telemetry.Constants.Enums; |
||||
|
using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; |
||||
|
using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors; |
||||
|
|
||||
|
internal sealed class ChromeDetector : SoftwareDetector |
||||
|
{ |
||||
|
public override string Name => "Chrome"; |
||||
|
|
||||
|
public async override Task<SoftwareInfo?> DetectAsync() |
||||
|
{ |
||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) |
||||
|
{ |
||||
|
var chromePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Google", "Chrome", "Application", "chrome.exe"); |
||||
|
if (File.Exists(chromePath)) |
||||
|
{ |
||||
|
return new SoftwareInfo(Name, GetFileVersion(chromePath), null, SoftwareType.Browser); |
||||
|
} |
||||
|
} |
||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) |
||||
|
{ |
||||
|
var chromePath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"; |
||||
|
if (File.Exists(chromePath)) |
||||
|
{ |
||||
|
var version = await ExecuteCommandAsync(chromePath, "--version"); |
||||
|
return new SoftwareInfo(Name, version, null, SoftwareType.Browser); |
||||
|
} |
||||
|
} |
||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) |
||||
|
{ |
||||
|
var chromePath = "/usr/bin/google-chrome"; |
||||
|
if (File.Exists(chromePath)) |
||||
|
{ |
||||
|
var version = await ExecuteCommandAsync(chromePath, "--version"); |
||||
|
return new SoftwareInfo(Name, version, null, SoftwareType.Browser); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Internal.Telemetry.Constants.Enums; |
||||
|
using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; |
||||
|
using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors; |
||||
|
|
||||
|
internal sealed class DotnetSdkDetector : SoftwareDetector |
||||
|
{ |
||||
|
public override string Name => "DotnetSdk"; |
||||
|
|
||||
|
public async override Task<SoftwareInfo?> DetectAsync() |
||||
|
{ |
||||
|
return new SoftwareInfo(Name, Environment.Version.ToString(), null, SoftwareType.DotnetSdk); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,45 @@ |
|||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Internal.Telemetry.Constants.Enums; |
||||
|
using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; |
||||
|
using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors; |
||||
|
|
||||
|
internal sealed class FireFoxDetector : SoftwareDetector |
||||
|
{ |
||||
|
public override string Name => "Firefox"; |
||||
|
public async override Task<SoftwareInfo?> DetectAsync() |
||||
|
{ |
||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) |
||||
|
{ |
||||
|
var firefoxPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Mozilla Firefox", "firefox.exe"); |
||||
|
if (File.Exists(firefoxPath)) |
||||
|
{ |
||||
|
return new SoftwareInfo(Name, GetFileVersion(firefoxPath), null, SoftwareType.Browser); |
||||
|
} |
||||
|
} |
||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) |
||||
|
{ |
||||
|
var firefoxPath = "/Applications/Firefox.app/Contents/MacOS/firefox"; |
||||
|
if (File.Exists(firefoxPath)) |
||||
|
{ |
||||
|
var version = await ExecuteCommandAsync(firefoxPath, "--version"); |
||||
|
return new SoftwareInfo(Name, version, null, SoftwareType.Browser); |
||||
|
} |
||||
|
} |
||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) |
||||
|
{ |
||||
|
var firefoxPath = "/usr/bin/firefox"; |
||||
|
if (File.Exists(firefoxPath)) |
||||
|
{ |
||||
|
var version = await ExecuteCommandAsync(firefoxPath, "--version"); |
||||
|
return new SoftwareInfo(Name, version, null, SoftwareType.Browser); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Internal.Telemetry.Constants.Enums; |
||||
|
using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; |
||||
|
using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors; |
||||
|
|
||||
|
internal sealed class MsEdgeDetector : SoftwareDetector |
||||
|
{ |
||||
|
public override string Name => "MsEdge"; |
||||
|
public async override Task<SoftwareInfo?> DetectAsync() |
||||
|
{ |
||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) |
||||
|
{ |
||||
|
var firefoxPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), |
||||
|
"Microsoft", "Edge", "Application", "msedge.exe"); |
||||
|
|
||||
|
if (File.Exists(firefoxPath)) |
||||
|
{ |
||||
|
return new SoftwareInfo(Name, GetFileVersion(firefoxPath), null, SoftwareType.Browser); |
||||
|
} |
||||
|
} |
||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) |
||||
|
{ |
||||
|
var edgePath = "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"; |
||||
|
if (File.Exists(edgePath)) |
||||
|
{ |
||||
|
var version = await ExecuteCommandAsync(edgePath, "--version"); |
||||
|
return new SoftwareInfo(Name, version, null, SoftwareType.Browser); |
||||
|
} |
||||
|
} |
||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) |
||||
|
{ |
||||
|
var edgePath = "/usr/bin/microsoft-edge"; |
||||
|
if (File.Exists(edgePath)) |
||||
|
{ |
||||
|
var version = await ExecuteCommandAsync(edgePath, "--version"); |
||||
|
return new SoftwareInfo(Name, version, null, SoftwareType.Browser); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Internal.Telemetry.Constants.Enums; |
||||
|
using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; |
||||
|
using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors; |
||||
|
|
||||
|
internal class NodeJsDetector : SoftwareDetector |
||||
|
{ |
||||
|
public override string Name => "Node.js"; |
||||
|
|
||||
|
public async override Task<SoftwareInfo?> DetectAsync() |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var output = await ExecuteCommandAsync("node", "-v"); |
||||
|
|
||||
|
if (output.IsNullOrWhiteSpace()) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
var version = output.Trim().TrimStart('v'); |
||||
|
|
||||
|
return new SoftwareInfo(Name, version, uiTheme: null, SoftwareType.Others); |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,77 @@ |
|||||
|
using System; |
||||
|
using System.Diagnostics; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Internal.Telemetry.Constants.Enums; |
||||
|
using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; |
||||
|
using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors; |
||||
|
|
||||
|
internal sealed class OperatingSystemDetector : SoftwareDetector |
||||
|
{ |
||||
|
public override string Name => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Windows" : |
||||
|
RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "macOS" : "Linux"; |
||||
|
|
||||
|
public async override Task<SoftwareInfo?> DetectAsync() |
||||
|
{ |
||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) |
||||
|
{ |
||||
|
return new SoftwareInfo(Name, Environment.OSVersion.Version.ToString(), null, SoftwareType.OperatingSystem); |
||||
|
} |
||||
|
|
||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) |
||||
|
{ |
||||
|
var version = await ExecuteCommandAsync("sw_vers", "-productVersion"); |
||||
|
return new SoftwareInfo(Name, version, GetMacUiTheme(), SoftwareType.OperatingSystem); |
||||
|
} |
||||
|
|
||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) |
||||
|
{ |
||||
|
var version = await ExecuteCommandAsync("lsb_release", "-ds") ?? await ExecuteCommandAsync("uname", "-r"); |
||||
|
return new SoftwareInfo(Name, version, await GetLinuxUiTheme(), SoftwareType.OperatingSystem); |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
private async Task<string?> GetLinuxUiTheme() |
||||
|
{ |
||||
|
var output = await ExecuteCommandAsync("gsettings", "get org.gnome.desktop.interface gtk-theme"); |
||||
|
|
||||
|
if (!output.IsNullOrWhiteSpace() && output.ToLowerInvariant().Contains("dark")) |
||||
|
{ |
||||
|
return "Dark"; |
||||
|
} |
||||
|
|
||||
|
return "Light"; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
private string? GetMacUiTheme() |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
using var process = new Process(); |
||||
|
process.StartInfo = new ProcessStartInfo |
||||
|
{ |
||||
|
FileName = "defaults", |
||||
|
Arguments = "read -g AppleInterfaceStyle", |
||||
|
RedirectStandardOutput = true, |
||||
|
RedirectStandardError = true, |
||||
|
UseShellExecute = false, |
||||
|
CreateNoWindow = true |
||||
|
}; |
||||
|
|
||||
|
process.Start(); |
||||
|
var output = process.StandardOutput.ReadToEnd().Trim(); |
||||
|
process.WaitForExit(); |
||||
|
|
||||
|
return output == "Dark" ? "Dark" : "Light"; |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
return "Light"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,100 @@ |
|||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using System.Threading.Tasks; |
||||
|
using System.Xml.Linq; |
||||
|
using Volo.Abp.Internal.Telemetry.Constants.Enums; |
||||
|
using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; |
||||
|
using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors; |
||||
|
|
||||
|
internal sealed class RiderDetector : SoftwareDetector |
||||
|
{ |
||||
|
public override string Name => "Rider"; |
||||
|
|
||||
|
public override Task<SoftwareInfo?> DetectAsync() |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
string baseConfigDir; |
||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) |
||||
|
{ |
||||
|
baseConfigDir = Path.Combine( |
||||
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), |
||||
|
"JetBrains"); |
||||
|
} |
||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) |
||||
|
{ |
||||
|
baseConfigDir = Path.Combine( |
||||
|
Environment.GetFolderPath(Environment.SpecialFolder.Personal), |
||||
|
"Library", "Application Support", "JetBrains"); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
baseConfigDir = Path.Combine( |
||||
|
Environment.GetFolderPath(Environment.SpecialFolder.Personal), |
||||
|
".config", "JetBrains"); |
||||
|
} |
||||
|
|
||||
|
if (!Directory.Exists(baseConfigDir)) |
||||
|
{ |
||||
|
return Task.FromResult<SoftwareInfo?>(null); |
||||
|
} |
||||
|
|
||||
|
var riderDirs = Directory |
||||
|
.GetDirectories(baseConfigDir, "Rider*") |
||||
|
.Select(dir => |
||||
|
{ |
||||
|
var name = Path.GetFileName(dir); |
||||
|
var verStr = name.Substring("Rider".Length); |
||||
|
return Version.TryParse(verStr, out var v) |
||||
|
? (Path: dir, Version: v) |
||||
|
: (Path: null, Version: null); |
||||
|
}) |
||||
|
.Where(x => x.Path != null) |
||||
|
.ToList(); |
||||
|
|
||||
|
if (!riderDirs.Any()) |
||||
|
{ |
||||
|
return Task.FromResult<SoftwareInfo?>(null); |
||||
|
} |
||||
|
|
||||
|
var latest = riderDirs |
||||
|
.OrderByDescending(x => x.Version) |
||||
|
.First(); |
||||
|
|
||||
|
var theme = string.Empty; |
||||
|
var colorsFile = Path.Combine(latest.Path!, "options", "colors.scheme.xml"); |
||||
|
if (File.Exists(colorsFile)) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var doc = XDocument.Load(colorsFile); |
||||
|
var schemeEl = doc |
||||
|
.Descendants("global_color_scheme") |
||||
|
.FirstOrDefault(); |
||||
|
var schemeName = schemeEl?.Attribute("name")?.Value; |
||||
|
if (!schemeName.IsNullOrEmpty()) |
||||
|
{ |
||||
|
theme = schemeName.IndexOf("dark", StringComparison.OrdinalIgnoreCase) >= 0 |
||||
|
? "Dark" |
||||
|
: "Light"; |
||||
|
} |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
//ignored
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return Task.FromResult<SoftwareInfo?>(new SoftwareInfo(Name, latest.Version?.ToString(), theme, |
||||
|
SoftwareType.Ide)); |
||||
|
} |
||||
|
catch (Exception e) |
||||
|
{ |
||||
|
return Task.FromResult<SoftwareInfo?>(null); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,132 @@ |
|||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using System.Text.Json; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.Internal.Telemetry.Constants.Enums; |
||||
|
using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; |
||||
|
using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors; |
||||
|
|
||||
|
internal sealed class VisualStudioCodeDetector : SoftwareDetector |
||||
|
{ |
||||
|
public override string Name => "Visual Studio Code"; |
||||
|
|
||||
|
public async override Task<SoftwareInfo?> DetectAsync() |
||||
|
{ |
||||
|
string? installDir = null; |
||||
|
string? settingsPath = null; |
||||
|
|
||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) |
||||
|
{ |
||||
|
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); |
||||
|
var progFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); |
||||
|
var candidates = new[] |
||||
|
{ |
||||
|
Path.Combine(localAppData, "Programs", "Microsoft VS Code"), |
||||
|
Path.Combine(progFiles, "Microsoft VS Code") |
||||
|
}; |
||||
|
installDir = candidates.FirstOrDefault(Directory.Exists); |
||||
|
|
||||
|
settingsPath = Path.Combine( |
||||
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), |
||||
|
"Code", "User", "globalStorage" ,"storage.json" |
||||
|
); |
||||
|
} |
||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) |
||||
|
{ |
||||
|
var app = "/Applications/Visual Studio Code.app"; |
||||
|
if (Directory.Exists(app)) |
||||
|
{ |
||||
|
installDir = app; |
||||
|
} |
||||
|
|
||||
|
settingsPath = Path.Combine( |
||||
|
Environment.GetFolderPath(Environment.SpecialFolder.Personal), |
||||
|
"Library", "Application Support", "Code", "User", "globalStorage", "storage.json" |
||||
|
); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var candidate = "/usr/share/code"; |
||||
|
if (Directory.Exists(candidate)) |
||||
|
{ |
||||
|
installDir = candidate; |
||||
|
} |
||||
|
|
||||
|
settingsPath = Path.Combine( |
||||
|
Environment.GetFolderPath(Environment.SpecialFolder.Personal), |
||||
|
".config", "Code", "User", "globalStorage", "storage.json" |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
if (installDir == null) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
Version? version = null; |
||||
|
var productJson = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) |
||||
|
? Path.Combine(installDir, "Contents", "Resources", "app", "product.json") |
||||
|
: Path.Combine(installDir, "resources", "app", "product.json"); |
||||
|
|
||||
|
if (File.Exists(productJson)) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
using var jsonDoc = JsonDocument.Parse(File.ReadAllText(productJson)); |
||||
|
var root = jsonDoc.RootElement; |
||||
|
if (root.TryGetProperty("version", out var versionProp)) |
||||
|
{ |
||||
|
var versionStr = versionProp.GetString(); |
||||
|
if (Version.TryParse(versionStr, out var v)) |
||||
|
{ |
||||
|
version = v; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (version == null) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
var theme = "Unknown"; |
||||
|
|
||||
|
if (File.Exists(settingsPath)) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
using var json = JsonDocument.Parse( File.ReadAllText(settingsPath)); |
||||
|
var root = json.RootElement; |
||||
|
if (root.TryGetProperty("theme", out var themeProp)) |
||||
|
{ |
||||
|
var themeName = themeProp.GetString() ?? ""; |
||||
|
if (themeName.IndexOf("dark", StringComparison.OrdinalIgnoreCase) >= 0) |
||||
|
{ |
||||
|
theme = "Dark"; |
||||
|
} |
||||
|
else if (themeName.IndexOf("light", StringComparison.OrdinalIgnoreCase) >= 0) |
||||
|
{ |
||||
|
theme = "Light"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
// ignored
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return new SoftwareInfo(Name, version?.ToString(), theme, SoftwareType.Ide); |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,106 @@ |
|||||
|
using System; |
||||
|
using System.Diagnostics; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using System.Xml.Linq; |
||||
|
using Volo.Abp.Internal.Telemetry.Constants.Enums; |
||||
|
using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; |
||||
|
using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Core; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Detectors; |
||||
|
|
||||
|
internal sealed class VisualStudioDetector : SoftwareDetector |
||||
|
{ |
||||
|
public override string Name => "Visual Studio"; |
||||
|
|
||||
|
public override Task<SoftwareInfo?> DetectAsync() |
||||
|
{ |
||||
|
var version = GetVisualStudioVersionViaVsWhere(); |
||||
|
var theme = GetVisualStudioTheme(); |
||||
|
|
||||
|
if (version == null) |
||||
|
{ |
||||
|
return Task.FromResult<SoftwareInfo?>(null); |
||||
|
} |
||||
|
|
||||
|
return Task.FromResult<SoftwareInfo?>(new SoftwareInfo( |
||||
|
name: Name, |
||||
|
version: version, |
||||
|
uiTheme: theme, |
||||
|
softwareType: SoftwareType.Ide)); |
||||
|
} |
||||
|
|
||||
|
private string? GetVisualStudioVersionViaVsWhere() |
||||
|
{ |
||||
|
var vswherePath = Path.Combine( |
||||
|
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), |
||||
|
"Microsoft Visual Studio", |
||||
|
"Installer", |
||||
|
"vswhere.exe"); |
||||
|
|
||||
|
if (!File.Exists(vswherePath)) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
var process = new Process |
||||
|
{ |
||||
|
StartInfo = new ProcessStartInfo |
||||
|
{ |
||||
|
FileName = vswherePath, |
||||
|
Arguments = "-latest -property catalog_productDisplayVersion", |
||||
|
RedirectStandardOutput = true, |
||||
|
UseShellExecute = false, |
||||
|
CreateNoWindow = true |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
process.Start(); |
||||
|
var output = process.StandardOutput.ReadToEnd().Trim(); |
||||
|
process.WaitForExit(); |
||||
|
|
||||
|
return string.IsNullOrWhiteSpace(output) ? null : output; |
||||
|
} |
||||
|
|
||||
|
private string? GetVisualStudioTheme() |
||||
|
{ |
||||
|
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); |
||||
|
|
||||
|
var vsSettingsDir = Path.Combine(localAppData, "Microsoft", "VisualStudio"); |
||||
|
|
||||
|
if (!Directory.Exists(vsSettingsDir)) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
var settingsPath = Directory.GetFiles(vsSettingsDir, "CurrentSettings*.vssettings", SearchOption.AllDirectories) |
||||
|
.OrderByDescending(File.GetLastWriteTime) |
||||
|
.FirstOrDefault(); |
||||
|
|
||||
|
if (string.IsNullOrEmpty(settingsPath)) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
var doc = XDocument.Load(settingsPath); |
||||
|
|
||||
|
var themeId = doc.Descendants("Theme") |
||||
|
.FirstOrDefault()?.Attribute("Id")?.Value; |
||||
|
|
||||
|
return themeId?.ToUpperInvariant() switch |
||||
|
{ |
||||
|
"{1DED0138-47CE-435E-84EF-9EC1F439B749}" => "Dark", |
||||
|
"{DE3DBBCD-F642-433C-8353-8F1DF4370ABA}" => "Light", |
||||
|
"{2DED0138-47CE-435E-84EF-9EC1F439B749}" => "Blue", |
||||
|
_ => "Unknown" |
||||
|
}; |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using Volo.Abp.Internal.Telemetry.EnvironmentInspection.Contracts; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.EnvironmentInspection.Providers; |
||||
|
|
||||
|
internal class SoftwareInfoProvider : ISoftwareInfoProvider , ISingletonDependency |
||||
|
{ |
||||
|
private readonly IEnumerable<ISoftwareDetector> _softwareDetectors; |
||||
|
|
||||
|
public SoftwareInfoProvider(IEnumerable<ISoftwareDetector> softwareDetectors) |
||||
|
{ |
||||
|
_softwareDetectors = softwareDetectors; |
||||
|
} |
||||
|
|
||||
|
public async Task<List<SoftwareInfo>> GetSoftwareInfoAsync() |
||||
|
{ |
||||
|
var result = new List<SoftwareInfo>(); |
||||
|
|
||||
|
foreach (var softwareDetector in _softwareDetectors) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var softwareInfo = await softwareDetector.DetectAsync(); |
||||
|
if (softwareInfo is not null) |
||||
|
{ |
||||
|
result.Add(softwareInfo); |
||||
|
} |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
//ignored
|
||||
|
} |
||||
|
} |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
} |
||||
@ -0,0 +1,135 @@ |
|||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Linq; |
||||
|
using System.Reflection; |
||||
|
using System.Text; |
||||
|
using System.Text.Json; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.Helpers; |
||||
|
|
||||
|
static internal class AbpProjectMetadataReader |
||||
|
{ |
||||
|
private const string AbpPackageSearchPattern = "*.abppkg"; |
||||
|
private const string AbpSolutionSearchPattern = "*.abpsln"; |
||||
|
private const int MaxDepth = 5; |
||||
|
public static AbpProjectMetaData? ReadProjectMetadata(Assembly assembly) |
||||
|
{ |
||||
|
var assemblyPath = assembly.Location; |
||||
|
try |
||||
|
{ |
||||
|
var projectDirectory = Path.GetDirectoryName(assemblyPath); |
||||
|
if (projectDirectory == null) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
var abpPackagePath = FindFileUpwards(projectDirectory, AbpPackageSearchPattern); |
||||
|
|
||||
|
if (abpPackagePath.IsNullOrEmpty()) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
var projectMetaData = ReadOrCreateMetadata(abpPackagePath); |
||||
|
|
||||
|
var abpSolutionPath = FindFileUpwards(projectDirectory, AbpSolutionSearchPattern); |
||||
|
|
||||
|
if (!abpSolutionPath.IsNullOrEmpty()) |
||||
|
{ |
||||
|
projectMetaData.AbpSlnPath = abpSolutionPath; |
||||
|
} |
||||
|
|
||||
|
return projectMetaData; |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static AbpProjectMetaData ReadOrCreateMetadata(string packagePath) |
||||
|
{ |
||||
|
|
||||
|
var fileContent = File.ReadAllText(packagePath); |
||||
|
var metadata = new AbpProjectMetaData(); |
||||
|
|
||||
|
using var document = JsonDocument.Parse(fileContent); |
||||
|
var root = document.RootElement; |
||||
|
|
||||
|
if (TryGetProjectId(root,out var projectId)) |
||||
|
{ |
||||
|
metadata.ProjectId = projectId; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
metadata.ProjectId = Guid.NewGuid(); |
||||
|
WriteProjectIdToPackageFile(root, packagePath, metadata.ProjectId.Value); |
||||
|
} |
||||
|
|
||||
|
if (root.TryGetProperty("role", out var roleElement) && |
||||
|
roleElement.ValueKind == JsonValueKind.String) |
||||
|
{ |
||||
|
metadata.Role = roleElement.GetString()!; |
||||
|
} |
||||
|
|
||||
|
return metadata; |
||||
|
} |
||||
|
|
||||
|
private static void WriteProjectIdToPackageFile(JsonElement root, string packagePath, Guid projectId) |
||||
|
{ |
||||
|
using var stream = new MemoryStream(); |
||||
|
using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true })) |
||||
|
{ |
||||
|
writer.WriteStartObject(); |
||||
|
|
||||
|
if (root.ValueKind == JsonValueKind.Object) |
||||
|
{ |
||||
|
foreach (var property in root.EnumerateObject()) |
||||
|
{ |
||||
|
if (property.Name != "projectId") |
||||
|
{ |
||||
|
property.WriteTo(writer); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writer.WriteString("projectId", projectId.ToString()); |
||||
|
writer.WriteEndObject(); |
||||
|
} |
||||
|
|
||||
|
var json = Encoding.UTF8.GetString(stream.ToArray()); |
||||
|
File.WriteAllText(packagePath, json); |
||||
|
} |
||||
|
|
||||
|
private static string? FindFileUpwards(string startingDir, string searchPattern) |
||||
|
{ |
||||
|
var currentDir = new DirectoryInfo(startingDir); |
||||
|
var currentDepth = 0; |
||||
|
|
||||
|
while (currentDir != null && currentDepth < MaxDepth) |
||||
|
{ |
||||
|
var file = currentDir.GetFiles(searchPattern).FirstOrDefault(); |
||||
|
if (file != null) |
||||
|
{ |
||||
|
return file.FullName; |
||||
|
} |
||||
|
|
||||
|
currentDir = currentDir.Parent; |
||||
|
currentDepth++; |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
private static bool TryGetProjectId(JsonElement element, out Guid projectId) |
||||
|
{ |
||||
|
if (element.TryGetProperty("projectId", out var projectIdElement) && |
||||
|
projectIdElement.ValueKind == JsonValueKind.String && |
||||
|
Guid.TryParse(projectIdElement.GetString(), out projectId)) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
projectId = Guid.Empty; |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.Helpers; |
||||
|
|
||||
|
internal class AbpProjectMetaData |
||||
|
{ |
||||
|
public Guid? ProjectId { get; set; } |
||||
|
public string? Role { get; set; } |
||||
|
public string? AbpSlnPath { get; set; } |
||||
|
} |
||||
@ -0,0 +1,42 @@ |
|||||
|
using System; |
||||
|
using System.Security.Cryptography; |
||||
|
using System.Text; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.Helpers; |
||||
|
|
||||
|
static internal class Cryptography |
||||
|
{ |
||||
|
private const string EncryptionKey = "AbpTelemetryStorageKey"; |
||||
|
|
||||
|
public static string Encrypt(string plainText) |
||||
|
{ |
||||
|
Check.NotNullOrEmpty(plainText, nameof(plainText)); |
||||
|
using var aes = Aes.Create(); |
||||
|
using var sha256 = SHA256.Create(); |
||||
|
|
||||
|
aes.Key = sha256.ComputeHash(Encoding.UTF8.GetBytes(EncryptionKey)); |
||||
|
aes.Mode = CipherMode.ECB; |
||||
|
aes.Padding = PaddingMode.PKCS7; |
||||
|
|
||||
|
var encryptor = aes.CreateEncryptor(); |
||||
|
var inputBytes = Encoding.UTF8.GetBytes(plainText); |
||||
|
var encryptedBytes = encryptor.TransformFinalBlock(inputBytes, 0, inputBytes.Length); |
||||
|
return Convert.ToBase64String(encryptedBytes); |
||||
|
} |
||||
|
|
||||
|
public static string Decrypt(string cipherText) |
||||
|
{ |
||||
|
Check.NotNullOrEmpty(cipherText, nameof(cipherText)); |
||||
|
using var aes = Aes.Create(); |
||||
|
using var sha256 = SHA256.Create(); |
||||
|
|
||||
|
aes.Key = sha256.ComputeHash(Encoding.UTF8.GetBytes(EncryptionKey)); |
||||
|
aes.Mode = CipherMode.ECB; |
||||
|
aes.Padding = PaddingMode.PKCS7; |
||||
|
|
||||
|
var decryptor = aes.CreateDecryptor(); |
||||
|
var inputBytes = Convert.FromBase64String(cipherText); |
||||
|
var decryptedBytes = decryptor.TransformFinalBlock(inputBytes, 0, inputBytes.Length); |
||||
|
return Encoding.UTF8.GetString(decryptedBytes); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Threading; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry.Helpers; |
||||
|
|
||||
|
static internal class MutexExecutor |
||||
|
{ |
||||
|
private const string MutexName = "Global\\MyFileReadMutex"; |
||||
|
private const int TimeoutMilliseconds = 3000; |
||||
|
|
||||
|
public static string? ReadFileSafely(string filePath) |
||||
|
{ |
||||
|
using var mutex = new Mutex(false, MutexName); |
||||
|
|
||||
|
if (!mutex.WaitOne(TimeoutMilliseconds)) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
if (!File.Exists(filePath)) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
return File.ReadAllText(filePath); |
||||
|
} |
||||
|
catch (IOException) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
mutex.ReleaseMutex(); |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
// Already released or abandoned
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Volo.Abp.Internal.Telemetry; |
||||
|
|
||||
|
public interface ITelemetryActivitySender |
||||
|
{ |
||||
|
Task TrySendQueuedActivitiesAsync(); |
||||
|
} |
||||