OfX 5.0.17
dotnet add package OfX --version 5.0.17
NuGet\Install-Package OfX -Version 5.0.17
<PackageReference Include="OfX" Version="5.0.17" />
<PackageVersion Include="OfX" Version="5.0.17" />
<PackageReference Include="OfX" />
paket add OfX --version 5.0.17
#r "nuget: OfX, 5.0.17"
#addin nuget:?package=OfX&version=5.0.17
#tool nuget:?package=OfX&version=5.0.17
OfX
public string XId { get; set; }
[XOf(nameof(XId))] public string X { get; set; }
OfX is an open-source, which focus on Attribute-based data mapping, streamlines data handling across services, reduces boilerplate code, and improves maintainability
[!WARNING]
All OfX* packages need to have the same version.
Project Highlights
Attribute-based Data Mapping in OfX is a feature that lets developers annotate properties in their data models with
custom attributes. These attributes define how and from where data should be fetched, eliminating repetitive code and
automating data retrieval.
For example, imagine a scenario where Service A needs a user’s name stored in Service B. With Attribute-based Data
Mapping, Service A can define a UserName property annotated with [UserOf(nameof(UserId))]
. This tells the system to
automatically retrieve the UserName based on UserId, without writing custom code each time.
Example:
// Basic Config
builder.Services.AddOfX(cfg =>
{
cfg.AddAttributesContainNamespaces(typeof(UserOfAttribute).Namespace!);
cfg.AddModelConfigurationsFromNamespaceContaining<SomeModelAssemblyMarker>();
});
// Define a custom OfXAttribute
public sealed class UserOfAttribute(string propertyName) : OfXAttribute(propertyName);
// Tell OfX which model the attribute applies to
[OfXConfigFor<UserOfAttribute>(nameof(Id), nameof(Name))]
public sealed class User
{
public string Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
// Add other properties as needed
}
// Sample DTO
public sealed class SomeDataResponse
{
public string Id { get; set; }
public string UserId { get; set; }
[UserOf(nameof(UserId), Expression = "Email")]
public string UserEmail { get; set; }
[UserOf(nameof(UserId))]
public string UserName { get; set; }
// Add other properties as needed
}
// You are touching the OfX!
The [UserOf]
annotation acts as a directive to automatically retrieve UserName
based on UserId
,you can also fetch
custom fields as Email
on the User Table using Expression like [UserOf(nameof(UserId), Expression="Email")]
. This
eliminates the need for manual mapping logic, freeing developers to focus on core functionality rather than data
plumbing.
Start with OfX
To install the OfX package, use the following NuGet command:
dotnet add package OfX
Or via the NuGet Package Manager:
Install-Package OfX
How to Use
1. Register OfX in the Dependency Injection Container
Add the OfX to your service configuration to register OfX:
builder.Services.AddOfX(cfg =>
{
cfg.AddAttributesContainNamespaces(typeof(WhereTheAttributeDefined).Assembly);
cfg.AddHandlersFromNamespaceContaining<SomeHandlerAssemblyMarker>(); //<- Add this one when you want to self-handle the request as the example at the end of this guide. Otherwise, if you install the package OfX-gRPC or OfX-Nats...(like OfX transport extension package), there is no need to add this one anymore!
cfg.AddReceivedPipelines(c => c.OfType(typeof(GenericReceivedPipeline<>).OfType<OtherReceivedPipeline>());
cfg.AddSendPipelines(c => c.OfType(typeof(GenericSendPipeline<>).OfType(typeof(OtherSendPipeline<>)));
// When you have the stronglyTypeId, you have to create the config how to resolve the Id(from string type) to StronglyTypeId
cfg.AddStronglyTypeIdConverter(a => a.OfType<StronglyTypeIdRegisters>());
cfg.AddModelConfigurationsFromNamespaceContaining<SomeModelAssemblyMarker>();
cfg.ThrowIfException(); // Add this when you want to handle the error and know why the errors are occupied
cfg.SetMaxObjectSpawnTimes(16); // Add this when you want to limit the maxObject spawn times. It mean you can be noticed that your objects are so complex...
});
Function Descriptions
AddAttributesContainNamespaces
cfg.AddAttributesContainNamespaces(typeof(WhereTheAttributeDefined).Assembly);
Registers assemblies that contain the attributes, used by OfX for data mapping.
The Attribute must be inherited from OfXAttribute
and they will be scanned by OfX!
Parameters:
Assembly
: The assembly containing the (OfX) attributes.
AddHandlersFromNamespaceContaining
cfg.AddHandlersFromNamespaceContaining<SomeHandlerAssemblyMarker>(); //<- Add this one when you want to self-handle the request as the example at the end of this guide. Otherwise, if you install the package OfX-gRPC or OfX-Nats...(like OfX transport extension package), there is no need to add this one anymore!
Add assemblies that contain handlers responsible for processing queries or commands for data retrieval.
Handlers are the execution units that resolve attributes applied to models.
If this function is not invoked. The default value ItemsResponse<OfXDataResponse>
is returned!
Parameters:
Type
: A marker type within the assembly that includes the handler implementations.
Example:
cfg.AddHandlersFromNamespaceContaining<SomeHandlerAssemblyMarker>();
Here, AddHandlersFromNamespaceContaining
is a type within the assembly where your handler logic resides.
AddReceivedPipelines
cfg.AddReceivedPipelines(c => c.OfType(typeof(GenericReceivedPipeline<>).OfType<OtherReceivedPipeline>());
When you want to create pipelines to handle the received request for OfXAttribute
. You should use it on the server,
where you fetching and response to client!
Parameters:
Action<ReceivedPipeline>
: add the pipelines.
Example:
cfg.AddSendPipelines(c => c.OfType(typeof(GenericSendPipeline<>).OfType(typeof(OtherSendPipeline<>)));
AddSendPipelines
cfg.AddSendPipelines(c => c.OfType(typeof(GenericSendPipeline<>).OfType(typeof(OtherSendPipeline<>)));
When you want to create pipelines to handle the send request for OfXAttribute
. You should use it on the client, where
you send request to get data!
Parameters:
Action<SendPipeline>
: add the pipelines.
Example:
cfg.AddReceivedPipelines(c => c.OfType(typeof(GenericPipeline<>)).OfType<OtherPipeline>());
AddStronglyTypeIdConverter
// When you have the stronglyTypeId, you have to create the config how to resolve the Id(from string type) to StronglyTypeId
cfg.AddStronglyTypeIdConverter(a => a.OfType<StronglyTypeIdRegisters>());
When your models(entities) are using Strongly Type Id, you have to configure to tell how OfX can convert from general ID type(string) to your strongly type ID.
Parameters:
Action<StronglyTypeIdRegister>
the strongly type ID register delegate.
OfType
You have to create a class and implement interface IStronglyTypeConverter<T>
, then you have to override 2 methods(
Convert
and CanConvert
) to help OfX convert from general Id type(string) to your strongly type.
Please check the example above!
AddModelConfigurationsFromNamespaceContaining
cfg.AddModelConfigurationsFromNamespaceContaining<SomeModelAssemblyMarker>();
Locate your models and OfX will dynamic create the handler relevant to Model and OfXAttribute
ThrowIfException
cfg.ThrowIfException(); // Add this when you want to handle the error and know why the errors are occupied
This function enables strict error handling within OfX
.
When added, it ensures that any exceptions encountered during data mapping, request handling, or pipeline execution are
not silently ignored but instead explicitly thrown.
This helps developers quickly identify and debug issues by surfacing errors, making it easier to track down problems in
the OfX processing flow.
SetMaxObjectSpawnTimes
cfg.SetMaxObjectSpawnTimes(16); // Add this when you want to limit the maxObject spawn times. It mean you can be noticed that your objects are so complex...
This function sets an upper limit on the number of times an object can be spawned during recursive data mapping.
By default (Max spawn times: 32
), OfX allows objects to be dynamically created and mapped, but in complex object
structures, excessive recursive mapping can lead to performance issues or infinite loops.
Setting maxTimes helps prevent excessive nesting by defining a safe threshold, ensuring that the mapping process remains
efficient and controlled.
2. Integrate the OfXAttribute
into Your Models, Entities, or DTOs
Apply the attribute to your properties like this:
public sealed class SomeDataResponse
{
public string Id { get; set; }
public string UserId { get; set; }
[UserOf(nameof(UserId), Expression = "Email")]
public string UserEmail { get; set; }
[UserOf(nameof(UserId))]
public string UserName { get; set; }
[UserOf(nameof(UserId), Expression = "ProvinceId")]
public string ProvinceId { get; set; }
[ProvinceOf(nameof(ProvinceId), Order = 1)]
public string ProvinceName { get; set; }
[ProvinceOf(nameof(ProvinceId), Expression = "Country.Name", Order = 1)]
public string CountryName { get; set; }
[ProvinceOf(nameof(ProvinceId), Expression = "CountryId", Order = 1)]
public string CountryId { get; set; }
[CountryOf(nameof(CountryId), Expression = "Provinces[0 asc Name].Name", Order = 2)]
public string Province { get; set; }
// Add other properties as needed
}
3. Annotate OfXConfigForAttribute
your models with OfXAttribute
to then
OfX
will dynamic create relevant proxy handler for model and OfXAttribute
Example:
[OfXConfigFor<UserOfAttribute>(nameof(Id), nameof(Name))]
public class User
{
public string Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string ProvinceId {get; set;}
}
4. Write the Handlers in Your Service to Fetch the Data(when you are using OfX
only).
Note:
If you use OfX-gRPC, OfX-Nats, OfX-RabbitMq... or other transport data layer(next version extension packages),
there are no need to create Handlers anymore, they should be dynamic proxy handlers!
Implement a handler to process data requests.
Example:
public class UserRequestHandler(): IMappableRequestHandler<UserOfAttribute>
{
public async Task<ItemsResponse<OfXDataResponse>> RequestAsync(RequestContext<UserOfAttribute> request)
{
// Implement data fetching logic here (e.g., via REST, RPC, or gRPC)
}
}
5. Unlock the Full Power of Expressions
🚀
Expressions in OfX enable you to fetch external data dynamically and powerfully. By leveraging these, you can go beyond default data fetching and define specific rules to access external resources effortlessly. Let’s dive into how * Expressions* work and what makes them so versatile.
Default Data vs. External Data
- Default Data: Automatically fetched using
OfX Attribute
. NoExpression
is required. - External Data: Define an
Expression
to fetch specific or relational data from other tables.
Here’s how you can harness the power of Expressions in different scenarios:
Fetching Data on the Same Table
Simple case: fetching additional fields from the same table.
public sealed class SomeDataResponse
{
public string Id { get; set; }
public string UserId { get; set; }
[UserOf(nameof(UserId), Expression = "Email")]
public string UserEmail { get; set; }
[UserOf(nameof(UserId))]
public string UserName { get; set; }
}
User
structure:
[OfXConfigFor<UserOfAttribute>(nameof(Id), nameof(Name))]
public sealed class User
{
public string Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
...
}
Generated SQL:
SELECT u."Id", u.Name, u."Email"
FROM "Users" AS u
WHERE u."Id" IN (@__SomeUserIds__)
Fetching Data from Navigated Tables
Expressions also support navigation through navigated tables.
public sealed class SomeDataResponse
{
...
[UserOf(nameof(UserId), Expression = "ProvinceId")]
public string ProvinceId { get; set; }
[ProvinceOf(nameof(ProvinceId), Expression = "Country.Name", Order = 1)]
public string CountryName { get; set; }
...
}
In this case, Expression = "Country.Name"
means:
Start from the
Provinces
table.Navigate to the
Country
property.Fetch the
Name
field from the Countries table.
Structures:
[OfXConfigFor<ProvinceOfAttribute>(nameof(Id), nameof(Name))]
public sealed class Province
{
public ProvinceId Id { get; set; }
public string Name { get; set; }
public CountryId CountryId { get; set; }
public Country Country { get; set; }
}
[OfXConfigFor<CountryOfAttribute>(nameof(Id), nameof(Name))]
public class Country
{
public CountryId Id { get; set; }
public string Name { get; set; }
public List<Province> Provinces { get; set; }
}
If the Countries
table have the single navigator(like Country
on the table Provinces
) to other table, you can
extend the Expression
to thousand kilometers 😄. Like this one:
Expression = "Country.[SingleNavigator]...[Universal]
.
Generated SQL:
SELECT p."Id", c."Name"
FROM "Provinces" AS p
LEFT JOIN "Countries" AS c ON p."CountryId" = c."Id"
WHERE p."Id" IN (@__SomeProvinceIds___)
Mapping Objects Dynamically.
public sealed class SomeDataResponse
{
...
[UserOf(nameof(UserId), Expression = "ProvinceId")]
public string ProvinceId { get; set; }
[ProvinceOf(nameof(ProvinceId), Expression = "Country", Order = 1)]
public CountryDTO Country { get; set; }
...
}
public sealed class CountryDTO
{
public string Id { get; set; }
public string Name {get; set;}
}
Note
: The DTO structure (e.g., CountryDTO
) must match the source model's structure.
- Only properties directly on the source model (e.g.,
Id
,Name
) are selected. - Navigators (e.g.,
Provinces
) are ignored.
Note
: When you map an object, the correlation DTO should have the same structure with Model
like the CountryDTO
above.
Array Mapping:
Unlock powerful features for mapping collections!
1. All Items: [asc|desc
Property
]
- Retrieves all items ordered by the specified property.
- Example:
[CountryOf(nameof(CountryId), Expression = "Provinces[asc Name]")]
public List<ProvinceDTO> Provinces { get; set; }
Note
: We will retrieve all the items of a collection on navigator property, like the Provinces
on the Countries
table.
2.Single Item: [0|-1
asc|desc
Property
]example above:
- Fetches the first (
0
) or last (-1
) item in the collection. - Example:
public sealed class SomeDataResponse
{
...
[ProvinceOf(nameof(ProvinceId), Expression = "CountryId", Order = 1)]
public string CountryId { get; set; }
[CountryOf(nameof(CountryId), Expression = "Provinces[0 asc Name]", Order = 2)]
public ProvinceDTO Province { get; set; }
...
}
When you select one item, you can navigate to the next level of the Table. Like this one:
public sealed class SomeDataResponse
{
...
[ProvinceOf(nameof(ProvinceId), Expression = "CountryId", Order = 1)]
public string CountryId { get; set; }
[CountryOf(nameof(CountryId), Expression = "Provinces[0 asc Name].Name", Order = 2)]
public string ProvinceName { get; set; }
...
}
3.Offset & Limit: [Offset
Limit
asc|desc
Property
]
- Retrieves a slice of the collection.
- Example:
public sealed class SomeDataResponse
{
...
[ProvinceOf(nameof(ProvinceId), Expression = "CountryId", Order = 1)]
public string CountryId { get; set; }
[CountryOf(nameof(CountryId), Expression = "Provinces[2 10 asc Name]", Order = 2)]
public List<ProvinceDTO> Provinces { get; set; }
...
}
6. OfX Attribute Order
On the
chapter 5. Unlock the Full Power of Expressions
we have dive into the Expression power.
On this one, we will explore the Order
on the OfX Attribute
.
Look at the first example model:
public sealed class SomeDataResponse
{
public string Id { get; set; }
public string UserId { get; set; }
[UserOf(nameof(UserId), Expression = "Email")]
public string UserEmail { get; set; }
[UserOf(nameof(UserId))]
public string UserName { get; set; }
[UserOf(nameof(UserId), Expression = "ProvinceId")]
public string ProvinceId { get; set; }
[ProvinceOf(nameof(ProvinceId), Order = 1)]
public string ProvinceName { get; set; }
[ProvinceOf(nameof(ProvinceId), Expression = "Country.Name", Order = 1)]
public string CountryName { get; set; }
[ProvinceOf(nameof(ProvinceId), Expression = "CountryId", Order = 1)]
public string CountryId { get; set; }
[CountryOf(nameof(CountryId), Expression = "Provinces[0 asc Name].Name", Order = 2)]
public string Province { get; set; }
// Add other properties as needed
}
You can define Order data using OfX Attributes
such as
[ProvinceOf(nameof(ProvinceId), Expression = "Country.Name", Order = 1)]
or
[CountryOf(nameof(CountryId), Expression = "Provinces[0 asc Name].Name", Order = 2)]
.
By default, the Order
value is 0, which means that properties marked with OfX Attributes
will be fetched and set
first. Afterward, properties will be fetched and set according to their defined Order values in ascending order.
You can also specify negative Order values like Order = -1
or Order = -2
... In such cases, properties with negative
Order values will still follow the same ordering rules and be processed accordingly.
When mapping data, imagine that you need Property A
to be resolved first. If the required data is available, it will
then be used as input to resolve the next set of Properties
, ensuring an organized and logical flow.
Conclusion:
The Expression feature in OfX
opens up endless possibilities for querying and mapping data across complex
relationships. Whether you're working with single properties, nested objects, or collections, OfX
has you covered.
Stay tuned for even more exciting updates as we expand the capabilities of Expressions
!
That all, Enjoy your moment!
Package Name | Description | .NET Version | Document |
---|---|---|---|
Core | |||
OfX | OfX core | 8.0, 9.0 | This Document |
Data Providers | |||
OfX-EFCore | This is the OfX extension package using EntityFramework to fetch data | 8.0, 9.0 | ReadMe |
OfX-MongoDb | This is the OfX extension package using MongoDb to fetch data | 8.0, 9.0 | ReadMe |
Integrations | |||
OfX-HotChocolate | OfX.HotChocolate is an integration package with HotChocolate for OfX. | 8.0, 9.0 | ReadMe |
Transports | |||
OfX-gRPC | OfX.gRPC is an extension package for OfX that leverages gRPC for efficient data transportation. | 8.0, 9.0 | ReadMe |
OfX-Kafka | OfX-Kafka is an extension package for OfX that leverages Kafka for efficient data transportation. | 8.0, 9.0 | ReadMe |
OfX-Nats | OfX-Nats is an extension package for OfX that leverages Nats for efficient data transportation. | 8.0, 9.0 | ReadMe |
OfX-RabbitMq | OfX-RabbitMq is an extension package for OfX that leverages RabbitMq for efficient data transportation. | 8.0, 9.0 | ReadMe |
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. |
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Logging (>= 8.0.0)
-
net9.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Logging (>= 8.0.0)
NuGet packages (8)
Showing the top 5 NuGet packages that depend on OfX:
Package | Downloads |
---|---|
OfX-EFCore
OfX extension. Use EntityFramework as Data Querying |
|
OfX-gRPC
OfX extension. Use gRPC as Data transporting |
|
OfX-Nats
Nats.io extension. Use Nats as Data transporting |
|
OfX-RabbitMq
OfX-RabbitMq extension. Use RabbitMq as Data transporting |
|
OfX-Kafka
OfX-Kafka extension. Use Kafka as Data transporting |
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
5.0.17 | 713 | 3/23/2025 |
5.0.16 | 468 | 3/18/2025 |
5.0.15 | 164 | 3/17/2025 |
5.0.14 | 438 | 3/9/2025 |
5.0.13 | 382 | 3/6/2025 |
5.0.12 | 302 | 3/6/2025 |
5.0.11 | 354 | 3/2/2025 |
5.0.10 | 198 | 2/28/2025 |
5.0.9 | 154 | 2/28/2025 |
5.0.8 | 151 | 2/28/2025 |
5.0.7 | 134 | 2/27/2025 |
5.0.6 | 123 | 2/26/2025 |
5.0.5 | 117 | 2/24/2025 |
5.0.4 | 144 | 2/21/2025 |
5.0.3 | 111 | 2/21/2025 |
5.0.2 | 149 | 2/19/2025 |
5.0.1 | 133 | 2/18/2025 |
5.0.0 | 125 | 2/17/2025 |
4.1.3 | 129 | 2/11/2025 |
4.1.2 | 129 | 2/11/2025 |
4.1.1 | 122 | 2/9/2025 |
4.1.0 | 122 | 2/8/2025 |
4.0.1 | 126 | 2/6/2025 |
4.0.0 | 126 | 2/4/2025 |
3.3.3 | 127 | 2/4/2025 |
3.3.2 | 138 | 2/3/2025 |
3.3.1 | 123 | 2/1/2025 |
3.3.0 | 117 | 1/22/2025 |
3.2.1 | 118 | 1/22/2025 |
3.2.0 | 113 | 1/21/2025 |
3.1.13 | 117 | 1/18/2025 |
3.1.12 | 116 | 1/16/2025 |
3.1.11 | 100 | 1/13/2025 |
3.1.10 | 100 | 1/12/2025 |
3.1.9 | 101 | 1/10/2025 |
3.1.8 | 98 | 1/8/2025 |
3.1.7 | 118 | 1/7/2025 |
3.1.6 | 114 | 1/6/2025 |
3.1.5 | 111 | 1/5/2025 |
3.1.4 | 125 | 1/4/2025 |
3.1.3 | 124 | 1/4/2025 |
3.1.2 | 136 | 1/4/2025 |
3.1.1 | 127 | 1/3/2025 |
3.1.0 | 106 | 12/31/2024 |
3.0.5 | 92 | 12/30/2024 |
3.0.4 | 89 | 12/30/2024 |
3.0.3 | 96 | 12/30/2024 |
3.0.2 | 100 | 12/30/2024 |
3.0.1 | 94 | 12/30/2024 |
3.0.0 | 100 | 12/30/2024 |
2.0.3 | 107 | 12/30/2024 |
2.0.2 | 100 | 12/29/2024 |
2.0.1 | 99 | 12/29/2024 |
2.0.0 | 97 | 12/28/2024 |
1.1.4 | 108 | 12/27/2024 |
1.1.3 | 101 | 12/26/2024 |
1.1.2 | 107 | 12/26/2024 |
1.1.1 | 104 | 12/26/2024 |
1.1.0 | 100 | 12/25/2024 |
1.0.8 | 107 | 12/25/2024 |
1.0.7 | 97 | 12/25/2024 |
1.0.6 | 119 | 12/25/2024 |
1.0.5 | 83 | 12/24/2024 |
1.0.4 | 99 | 12/24/2024 |
1.0.3 | 87 | 12/24/2024 |
1.0.2 | 84 | 12/24/2024 |
1.0.1 | 87 | 12/24/2024 |
1.0.0 | 84 | 12/23/2024 |