diff --git a/OfX.sln.DotSettings b/OfX.sln.DotSettings index 548fbab..b503dfd 100644 --- a/OfX.sln.DotSettings +++ b/OfX.sln.DotSettings @@ -1,3 +1,4 @@  True - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/README.md b/README.md index e000154..c397b38 100644 --- a/README.md +++ b/README.md @@ -129,3 +129,4 @@ Enjoy your moment! | [OfX](https://www.nuget.org/packages/OfX/) | OfX core | 8.0, 9.0 | This Document | | [OfX-EFCore](https://www.nuget.org/packages/OfX-EFCore/) | This is the OfX extension package using EntityFramework to fetch data | 8.0, 9.0 | [ReadMe](https://github.com/quyvu01/OfX/blob/main/src/OfX.EntityFrameworkCore/README.md) | | [OfX-gRPC](https://www.nuget.org/packages/OfX-gRPC/) | OfX.gRPC is an extension package for OfX that leverages gRPC for efficient data transportation. | 8.0, 9.0 | [ReadMe](https://github.com/quyvu01/OfX/blob/main/src/OfX.Grpc/README.md) | +| [OfX-Nats](https://www.nuget.org/packages/OfX-Nats/) | OfX-Nats is an extension package for OfX that leverages Nats for efficient data transportation. | 8.0, 9.0 | [ReadMe](https://github.com/quyvu01/OfX/blob/dev-nats/src/OfX.Nats/README.md) | \ No newline at end of file diff --git a/src/OfX.EntityFrameworkCore/OfX.EntityFrameworkCore.csproj b/src/OfX.EntityFrameworkCore/OfX.EntityFrameworkCore.csproj index 66ca3e0..271f64f 100644 --- a/src/OfX.EntityFrameworkCore/OfX.EntityFrameworkCore.csproj +++ b/src/OfX.EntityFrameworkCore/OfX.EntityFrameworkCore.csproj @@ -4,7 +4,7 @@ enable net9.0;net8.0 default - 3.1.1 + 3.1.2 Quy Vu OfX-EFCore OfX extension. Use EntityFramework as Data Querying diff --git a/src/OfX.EntityFrameworkCore/README.md b/src/OfX.EntityFrameworkCore/README.md index c46039e..a64d2af 100644 --- a/src/OfX.EntityFrameworkCore/README.md +++ b/src/OfX.EntityFrameworkCore/README.md @@ -1,6 +1,6 @@ -# OfX-Nats +# OfX.EntityFrameworkCore -OfX-Nats is an extension package for OfX that leverages Nats for efficient data transportation. This package provides a high-performance, strongly-typed communication layer for OfX’s Attribute-based Data Mapping, enabling streamlined data retrieval across distributed systems. +OfX.EntityFrameworkCore is an extension package for OfX that integrates with Entity Framework Core to simplify data fetching by leveraging attribute-based data mapping. This extension streamlines data retrieval using EF Core, reducing boilerplate code and improving maintainability. [Demo Project!](https://github.com/quyvu01/TestOfX-Demo) @@ -8,61 +8,69 @@ OfX-Nats is an extension package for OfX that leverages Nats for efficient data ## Introduction -Nats-based Transport: Implements Nats to handle data communication between services, providing a fast, secure, and scalable solution. +OfX.EntityFrameworkCore extends the core OfX library by providing seamless integration with Entity Framework Core. This enables developers to automatically map and retrieve data directly from a database, leveraging the power of EF Core along with attribute-based data mapping. + +For example, suppose you have a `UserId` property in your model, and you want to fetch the corresponding `UserName` and `Email` fields from the database. By using OfX.EntityFrameworkCore, you can annotate your model with attributes, and the library will handle data fetching for you. --- ## Installation -To install the OfX-Nats package, use the following NuGet command: +To install the OfX.EntityFrameworkCore package, use the following NuGet command: ```bash -dotnet add package OfX-Nats +dotnet add package OfX-EFCore ``` Or via the NuGet Package Manager: ```bash -Install-Package OfX-Nats +Install-Package OfX-EFCore ``` --- ## How to Use -### 1. Register OfX-Nats - -Add OfX-Nats to your service configuration during application startup: +### 1. Register OfX.EntityFrameworkCore -For Client: +Add OfX.EntityFrameworkCore to your service configuration during application startup: ```csharp builder.Services.AddOfXEntityFrameworkCore(cfg => { - cfg.AddContractsContainNamespaces(typeof(SomeContractAssemblyMarker).Assembly); + cfg.AddAttributesContainNamespaces(typeof(WhereTheAttributeDefined).Assembly); cfg.AddHandlersFromNamespaceContaining(); - cfg.AddNats(config => config - config.UseNats((context, bus) => - { - bus.Host(host, c => - { - c.Username(userName); - c.Password(password); - }); - bus.ConfigureEndpoints(context); - }); - ); - +}) +.AddOfXEFCore(options => +{ + options.AddDbContexts(typeof(TestDbContext)); + options.AddModelConfigurationsFromNamespaceContaining(); }); ``` -That All, enjoy your moment! +After installing the package OfX-EFCore, you can use the method `AddDbContexts()`, which takes `DbContext(s)` to executing. + +### 2. Mark the model you want to use with OfXAttribute +Example: + +```csharp +[OfXConfigFor(nameof(Id), nameof(Name))] +public class User +{ + public string Id { get; set; } + public string Name { get; set; } + public string Email { get; set; } +} +``` +That all! Let go to the moon! -| Package Name | Description | .NET Version | Document | -|----------------------------------------------------------|-------------------------------------------------------------------------------------------------|--------------|------------------------------------------------------------------------------------------| -| [OfX](https://www.nuget.org/packages/OfX/) | OfX core | 8.0, 9.0 | [ReadMe](https://github.com/quyvu01/OfX/blob/main/README.md) | -| [OfX-EFCore](https://www.nuget.org/packages/OfX-EFCore/) | This is the OfX extension package using EntityFramework to fetch data | 8.0, 9.0 | [ReadMe](https://github.com/quyvu01/OfX/blob/main/src/OfX.EntityFrameworkCore/README.md) | -| [OfX-gRPC](https://www.nuget.org/packages/OfX-gRPC/) | OfX-gRPC is an extension package for OfX that leverages gRPC for efficient data transportation. | 8.0, 9.0 | [ReadMe](https://github.com/quyvu01/OfX/blob/main/src/OfX.Grpc/README.md) | -| [OfX-Nats](https://www.nuget.org/packages/OfX-Nats/) | OfX-Nats is an extension package for OfX that leverages Nats for efficient data transportation. | 8.0, 9.0 | This Document | +Note: In this release, Id is exclusively supported as a string. But hold tight—I'm gearing up to blow your mind with the next update! Stay tuned! +| Package Name | Description | .NET Version | Document | +|----------------------------------------------------------|-------------------------------------------------------------------------------------------------|--------------|-------------------------------------------------------------------------------| +| [OfX](https://www.nuget.org/packages/OfX/) | OfX core | 8.0, 9.0 | [ReadMe](https://github.com/quyvu01/OfX/blob/main/README.md) | +| [OfX-EFCore](https://www.nuget.org/packages/OfX-EFCore/) | This is the OfX extension package using EntityFramework to fetch data | 8.0, 9.0 | This Document | +| [OfX-gRPC](https://www.nuget.org/packages/OfX-gRPC/) | OfX.gRPC is an extension package for OfX that leverages gRPC for efficient data transportation. | 8.0, 9.0 | [ReadMe](https://github.com/quyvu01/OfX/blob/main/src/OfX.Grpc/README.md) | +| [OfX-Nats](https://www.nuget.org/packages/OfX-Nats/) | OfX-Nats is an extension package for OfX that leverages Nats for efficient data transportation. | 8.0, 9.0 | [ReadMe](https://github.com/quyvu01/OfX/blob/dev-nats/src/OfX.Nats/README.md) | --- \ No newline at end of file diff --git a/src/OfX.Grpc/Exceptions/OfXGrpcExceptions.cs b/src/OfX.Grpc/Exceptions/OfXGrpcExceptions.cs index cda8884..1b82ab5 100644 --- a/src/OfX.Grpc/Exceptions/OfXGrpcExceptions.cs +++ b/src/OfX.Grpc/Exceptions/OfXGrpcExceptions.cs @@ -16,7 +16,4 @@ public sealed class AttributeTypesCannotBeNull() public sealed class CannotDeserializeOfXAttributeType(string type) : Exception($"The OfX Attribute seems not a part of this application: {type}!"); - - public sealed class CannotFindHandlerForOfAttribute(Type type) - : Exception($"Cannot find handler for OfXAttribute type: {type.Name}!"); } \ No newline at end of file diff --git a/src/OfX.Grpc/Extensions/GrpcExtensions.cs b/src/OfX.Grpc/Extensions/GrpcExtensions.cs index 8a76dfe..ec4fc89 100644 --- a/src/OfX.Grpc/Extensions/GrpcExtensions.cs +++ b/src/OfX.Grpc/Extensions/GrpcExtensions.cs @@ -5,9 +5,9 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using OfX.Abstractions; using OfX.Extensions; +using OfX.Grpc.Abstractions; using OfX.Grpc.ApplicationModels; using OfX.Grpc.Delegates; -using OfX.Grpc.HandlersInstaller; using OfX.Grpc.Servers; using OfX.Helpers; using OfX.Queries.OfXQueries; @@ -19,12 +19,12 @@ namespace OfX.Grpc.Extensions; public static class GrpcExtensions { private static readonly TimeSpan defaultRequestTimeout = TimeSpan.FromSeconds(3); + public static void AddGrpcClients(this OfXRegister ofXRegister, Action options) { var newClientsRegister = new GrpcClientsRegister(); options.Invoke(newClientsRegister); var hostMapAttributes = newClientsRegister.HostMapAttributes; - var attributesRegister = hostMapAttributes.Values.SelectMany(a => a); ofXRegister.ServiceCollection.TryAddScoped(_ => attributeType => async (query, context) => { @@ -39,7 +39,8 @@ public static void AddGrpcClients(this OfXRegister ofXRegister, Action), [..ofXRegister.OfXAttributeTypes]); } private static async Task GetOfXItemsAsync(string serverHost, IContext context, diff --git a/src/OfX.Grpc/OfX.Grpc.csproj b/src/OfX.Grpc/OfX.Grpc.csproj index 2872c5e..5128b8a 100644 --- a/src/OfX.Grpc/OfX.Grpc.csproj +++ b/src/OfX.Grpc/OfX.Grpc.csproj @@ -4,7 +4,7 @@ enable net9.0;net8.0 default - 3.1.1 + 3.1.2 Quy Vu OfX-gRPC OfX extension. Use gRPC as Data transporting diff --git a/src/OfX.Grpc/README.md b/src/OfX.Grpc/README.md index 004fd0d..3015888 100644 --- a/src/OfX.Grpc/README.md +++ b/src/OfX.Grpc/README.md @@ -37,7 +37,7 @@ Add OfX-gRPC to your service configuration during application startup: For Client: ```csharp -builder.Services.AddOfXEntityFrameworkCore(cfg => +builder.Services.AddOfX(cfg => { cfg.AddContractsContainNamespaces(typeof(SomeContractAssemblyMarker).Assembly); cfg.AddHandlersFromNamespaceContaining(); @@ -72,5 +72,5 @@ That All, enjoy your moment! | [OfX](https://www.nuget.org/packages/OfX/) | OfX core | 8.0, 9.0 | [ReadMe](https://github.com/quyvu01/OfX/blob/main/README.md) | | [OfX-EFCore](https://www.nuget.org/packages/OfX-EFCore/) | This is the OfX extension package using EntityFramework to fetch data | 8.0, 9.0 | [ReadMe](https://github.com/quyvu01/OfX/blob/main/src/OfX.EntityFrameworkCore/README.md) | | [OfX-gRPC](https://www.nuget.org/packages/OfX-gRPC/) | OfX.gRPC is an extension package for OfX that leverages gRPC for efficient data transportation. | 8.0, 9.0 | This Document | - +| [OfX-Nats](https://www.nuget.org/packages/OfX-Nats/) | OfX-Nats is an extension package for OfX that leverages Nats for efficient data transportation. | 8.0, 9.0 | [ReadMe](https://github.com/quyvu01/OfX/blob/dev-nats/src/OfX.Nats/README.md) | --- \ No newline at end of file diff --git a/src/OfX.Grpc/Servers/OfXGrpcServer.cs b/src/OfX.Grpc/Servers/OfXGrpcServer.cs index e7ad23c..6bfdadf 100644 --- a/src/OfX.Grpc/Servers/OfXGrpcServer.cs +++ b/src/OfX.Grpc/Servers/OfXGrpcServer.cs @@ -1,10 +1,9 @@ -using System.Collections.Concurrent; using System.Diagnostics; -using System.Reflection; using Grpc.Core; using Microsoft.Extensions.DependencyInjection; using OfX.Abstractions; using OfX.Cached; +using OfX.Exceptions; using OfX.Grpc.Exceptions; using OfX.Implementations; using OfX.Responses; @@ -13,11 +12,6 @@ namespace OfX.Grpc.Servers; public sealed class OfXGrpcServer(IServiceProvider serviceProvider) : OfXTransportService.OfXTransportServiceBase { - private const string ExecuteAsync = nameof(ExecuteAsync); - - private static readonly Lazy> MethodInfoStorage = - new(() => new ConcurrentDictionary()); - public override async Task GetItems(GetOfXGrpcQuery request, ServerCallContext context) { try @@ -27,18 +21,15 @@ public override async Task GetItems(GetOfXGrpcQuery reques if (attributeType is null) throw new OfXGrpcExceptions.CannotDeserializeOfXAttributeType(request.AttributeAssemblyType); - if (!OfXCached.QueryMapHandler.TryGetValue(attributeType, out var handlerType)) - throw new OfXGrpcExceptions.CannotFindHandlerForOfAttribute(attributeType); + if (!OfXCached.AttributeMapHandler.TryGetValue(attributeType, out var handlerType)) + throw new OfXException.CannotFindHandlerForOfAttribute(attributeType); var modelArg = handlerType.GetGenericArguments()[0]; var pipeline = serviceProvider .GetRequiredService(typeof(ReceivedPipelinesImpl<,>).MakeGenericType(modelArg, attributeType)); - var pipelineMethod = MethodInfoStorage.Value.GetOrAdd(attributeType, q => pipeline.GetType().GetMethods() - .FirstOrDefault(m => - m.Name == ExecuteAsync && m.GetParameters() is { Length: 1 } parameters && - parameters[0].ParameterType == typeof(RequestContext<>).MakeGenericType(q))); + var pipelineMethod = OfXCached.GetPipelineMethodByAttribute(pipeline, attributeType); var requestContextType = typeof(RequestContextImpl<>).MakeGenericType(attributeType); diff --git a/src/OfX.Nats/Abstractions/INatsRequester.cs b/src/OfX.Nats/Abstractions/INatsRequester.cs new file mode 100644 index 0000000..f112136 --- /dev/null +++ b/src/OfX.Nats/Abstractions/INatsRequester.cs @@ -0,0 +1,10 @@ +using OfX.Abstractions; +using OfX.Attributes; +using OfX.Responses; + +namespace OfX.Nats.Abstractions; + +public interface INatsRequester where TAttribute : OfXAttribute +{ + Task> RequestAsync(RequestContext requestContext); +} \ No newline at end of file diff --git a/src/OfX.Nats/Abstractions/IOfXNatsClient.cs b/src/OfX.Nats/Abstractions/IOfXNatsClient.cs new file mode 100644 index 0000000..232c01a --- /dev/null +++ b/src/OfX.Nats/Abstractions/IOfXNatsClient.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; +using OfX.Abstractions; +using OfX.Attributes; +using OfX.Responses; + +namespace OfX.Nats.Abstractions; + +public interface IOfXNatsClient : IMappableRequestHandler where TAttribute : OfXAttribute +{ + IServiceProvider ServiceProvider { get; } + + async Task> IMappableRequestHandler.RequestAsync( + RequestContext context) + { + var natRequesterService = ServiceProvider.GetRequiredService>(); + var result = await natRequesterService.RequestAsync(context); + return result; + } +} \ No newline at end of file diff --git a/src/OfX.Nats/ApplicationModels/NatsClient.cs b/src/OfX.Nats/ApplicationModels/NatsClient.cs new file mode 100644 index 0000000..1d5c2ba --- /dev/null +++ b/src/OfX.Nats/ApplicationModels/NatsClient.cs @@ -0,0 +1,13 @@ +namespace OfX.Nats.ApplicationModels; + +public class NatsClient +{ + public string NatsHost { get; private set; } + public NatsCredential NatsCredential { get; } = new(); + + public void Host(string host, Action options = null) + { + NatsHost = host; + options?.Invoke(NatsCredential); + } +} \ No newline at end of file diff --git a/src/OfX.Nats/ApplicationModels/NatsClientRegister.cs b/src/OfX.Nats/ApplicationModels/NatsClientRegister.cs new file mode 100644 index 0000000..ed6b518 --- /dev/null +++ b/src/OfX.Nats/ApplicationModels/NatsClientRegister.cs @@ -0,0 +1,11 @@ +namespace OfX.Nats.ApplicationModels; + +public class NatsClientRegister +{ + public NatsClient NatsClient { get; } = new(); + + public void UseNats(Action options) + { + options.Invoke(NatsClient); + } +} \ No newline at end of file diff --git a/src/OfX.Nats/ApplicationModels/NatsCredential.cs b/src/OfX.Nats/ApplicationModels/NatsCredential.cs new file mode 100644 index 0000000..18e5bdc --- /dev/null +++ b/src/OfX.Nats/ApplicationModels/NatsCredential.cs @@ -0,0 +1,9 @@ +namespace OfX.Nats.ApplicationModels; + +public class NatsCredential +{ + public string NatsUserName { get; private set; } + public string NatsPassword { get; private set; } + public void UserName(string userName) => NatsUserName = userName; + public void Password(string password) => NatsPassword = password; +} \ No newline at end of file diff --git a/src/OfX.Nats/Extensions/NatsExtensions.cs b/src/OfX.Nats/Extensions/NatsExtensions.cs new file mode 100644 index 0000000..4c7b3c4 --- /dev/null +++ b/src/OfX.Nats/Extensions/NatsExtensions.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.DependencyInjection; +using NATS.Client; +using OfX.Nats.Abstractions; +using OfX.Nats.ApplicationModels; +using OfX.Nats.Implementations; +using OfX.Nats.Servers; +using OfX.Registries; + +namespace OfX.Nats.Extensions; + +public static class NatsExtensions +{ + public static void AddNats(this OfXRegister ofXRegister, Action options) + { + var newClientsRegister = new NatsClientRegister(); + options.Invoke(newClientsRegister); + // Register NATS connection as a singleton + ofXRegister.ServiceCollection.AddSingleton(_ => + { + var connectionFactory = new ConnectionFactory(); + var natsOptions = ConnectionFactory.GetDefaultOptions(); + natsOptions.AllowReconnect = true; + natsOptions.MaxReconnect = -1; + natsOptions.ReconnectWait = 2000; + natsOptions.Timeout = 5000; + natsOptions.Url = newClientsRegister.NatsClient.NatsHost; + natsOptions.User = newClientsRegister.NatsClient.NatsCredential.NatsUserName; + natsOptions.Password = newClientsRegister.NatsClient.NatsCredential.NatsPassword; + return connectionFactory.CreateConnection(natsOptions); + }); + ClientsRegister(ofXRegister.ServiceCollection); + Clients.ClientsInstaller.InstallMappableRequestHandlers(ofXRegister.ServiceCollection, + typeof(IOfXNatsClient<>), [..ofXRegister.OfXAttributeTypes]); + } + + private static void ClientsRegister(IServiceCollection serviceCollection) => + serviceCollection.AddScoped(typeof(INatsRequester<>), typeof(NatsRequester<>)); + + public static void StartNatsServerAsync(this IServiceProvider serviceProvider) + { + var serverListening = new NatsServersListening(serviceProvider); + serverListening.StartAsync(); + } +} \ No newline at end of file diff --git a/src/OfX.Nats/Implementations/NatsRequester.cs b/src/OfX.Nats/Implementations/NatsRequester.cs new file mode 100644 index 0000000..879e726 --- /dev/null +++ b/src/OfX.Nats/Implementations/NatsRequester.cs @@ -0,0 +1,27 @@ +using System.Text; +using System.Text.Json; +using NATS.Client; +using OfX.Abstractions; +using OfX.Attributes; +using OfX.Nats.Abstractions; +using OfX.Nats.Messages; +using OfX.Responses; + +namespace OfX.Nats.Implementations; + +public sealed class NatsRequester(IConnection connection) + : INatsRequester where TAttribute : OfXAttribute +{ + public async Task> RequestAsync(RequestContext requestContext) + { + var natsMessageWrapped = new NatsMessageRequestWrapped + { + Query = requestContext.Query, + Headers = requestContext.Headers + }; + var reply = await connection.RequestAsync(natsMessageWrapped.Subject, natsMessageWrapped.GetMessageSerialize(), + requestContext.CancellationToken); + var response = Encoding.UTF8.GetString(reply.Data); + return JsonSerializer.Deserialize>(response); + } +} \ No newline at end of file diff --git a/src/OfX.Nats/Messages/NatsMessageReceived.cs b/src/OfX.Nats/Messages/NatsMessageReceived.cs new file mode 100644 index 0000000..badf0a6 --- /dev/null +++ b/src/OfX.Nats/Messages/NatsMessageReceived.cs @@ -0,0 +1,13 @@ +namespace OfX.Nats.Messages; + +internal class NatsMessageReceived +{ + public Dictionary Headers { get; set; } + public MessageRequestOf Query { get; set; } +} + +internal class MessageRequestOf +{ + public List SelectorIds { get; set; } + public string Expression { get; set; } +} \ No newline at end of file diff --git a/src/OfX.Nats/Messages/NatsMessageRequestWrapped.cs b/src/OfX.Nats/Messages/NatsMessageRequestWrapped.cs new file mode 100644 index 0000000..7d53041 --- /dev/null +++ b/src/OfX.Nats/Messages/NatsMessageRequestWrapped.cs @@ -0,0 +1,24 @@ +using System.Text; +using System.Text.Json; +using OfX.Abstractions; +using OfX.Attributes; +using OfX.Helpers; +using OfX.Implementations; + +namespace OfX.Nats.Messages; + +public class NatsMessageRequestWrapped where TAttribute : OfXAttribute +{ + public Dictionary Headers { get; set; } + public RequestOf Query { get; set; } + public byte[] GetMessageSerialize() => Encoding.UTF8.GetBytes(JsonSerializer.Serialize(this)); + + public RequestContext GetMessageDeserialize(byte[] message) + { + var messageData = Encoding.UTF8.GetString(message); + var messageWrapped = JsonSerializer.Deserialize>(messageData); + return new RequestContextImpl(messageWrapped.Query, messageWrapped.Headers, CancellationToken.None); + } + + public string Subject => typeof(TAttribute).GetAssemblyName(); +} \ No newline at end of file diff --git a/src/OfX.Nats/OfX.Nats.csproj b/src/OfX.Nats/OfX.Nats.csproj index 615ff9d..43f4c80 100644 --- a/src/OfX.Nats/OfX.Nats.csproj +++ b/src/OfX.Nats/OfX.Nats.csproj @@ -2,11 +2,18 @@ enable - enable net9.0;net8.0 default - 3.1.1 + 3.1.2 + Quy Vu + OfX-Nats + Nats.io extension. Use Nats as Data transporting + OfX;mappable;open-source;Nats.io + https://github.com/quyvu01/OfX OfX.png + true + README.md + LICENSE @@ -19,5 +26,9 @@ OfX.png + + + + diff --git a/src/OfX.Nats/README.md b/src/OfX.Nats/README.md index 00c6c39..5f8a679 100644 --- a/src/OfX.Nats/README.md +++ b/src/OfX.Nats/README.md @@ -37,21 +37,12 @@ Add OfX-Nats to your service configuration during application startup: For Client: ```csharp -builder.Services.AddOfXEntityFrameworkCore(cfg => +builder.Services.AddOfX(cfg => { cfg.AddContractsContainNamespaces(typeof(SomeContractAssemblyMarker).Assembly); cfg.AddHandlersFromNamespaceContaining(); cfg.AddNats(config => config - config.UseNats((context, bus) => - { - bus.Host(host, c => - { - c.Username(userName); - c.Password(password); - }); - bus.ConfigureEndpoints(context); - }); - ); + .UseNats(c => c.Host("nats://localhost:4222"))); }); ``` @@ -62,6 +53,7 @@ That All, enjoy your moment! |----------------------------------------------------------|-------------------------------------------------------------------------------------------------|--------------|------------------------------------------------------------------------------------------| | [OfX](https://www.nuget.org/packages/OfX/) | OfX core | 8.0, 9.0 | [ReadMe](https://github.com/quyvu01/OfX/blob/main/README.md) | | [OfX-EFCore](https://www.nuget.org/packages/OfX-EFCore/) | This is the OfX extension package using EntityFramework to fetch data | 8.0, 9.0 | [ReadMe](https://github.com/quyvu01/OfX/blob/main/src/OfX.EntityFrameworkCore/README.md) | -| [OfX-gRPC](https://www.nuget.org/packages/OfX-gRPC/) | OfX.gRPC is an extension package for OfX that leverages gRPC for efficient data transportation. | 8.0, 9.0 | This Document | +| [OfX-gRPC](https://www.nuget.org/packages/OfX-gRPC/) | OfX-gRPC is an extension package for OfX that leverages gRPC for efficient data transportation. | 8.0, 9.0 | [ReadMe](https://github.com/quyvu01/OfX/blob/main/src/OfX.Grpc/README.md) | +| [OfX-Nats](https://www.nuget.org/packages/OfX-Nats/) | OfX-Nats is an extension package for OfX that leverages Nats for efficient data transportation. | 8.0, 9.0 | This Document | --- \ No newline at end of file diff --git a/src/OfX.Nats/Servers/NatsServersListening.cs b/src/OfX.Nats/Servers/NatsServersListening.cs new file mode 100644 index 0000000..0ef0b27 --- /dev/null +++ b/src/OfX.Nats/Servers/NatsServersListening.cs @@ -0,0 +1,56 @@ +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.DependencyInjection; +using NATS.Client; +using OfX.Abstractions; +using OfX.Cached; +using OfX.Exceptions; +using OfX.Extensions; +using OfX.Helpers; +using OfX.Implementations; +using OfX.Nats.Messages; +using OfX.Responses; + +namespace OfX.Nats.Servers; + +public class NatsServersListening(IServiceProvider serviceProvider) +{ + public void StartAsync() + { + var connection = serviceProvider.GetRequiredService(); + var serviceTypes = typeof(IQueryOfHandler<,>); + var handlers = OfXCached.AttributeMapHandler.Values.ToList(); + handlers.Where(a => a.IsGenericType && a.GetGenericTypeDefinition() == serviceTypes) + .Select(a => a.GetGenericArguments()[1]) + .ForEach(attributeType => connection.SubscribeAsync(attributeType.GetAssemblyName(), (_, args) => + { + var request = Encoding.UTF8.GetString(args.Message.Data); + var messageDeserialize = JsonSerializer.Deserialize(request); + + if (!OfXCached.AttributeMapHandler.TryGetValue(attributeType, out var handlerType)) + throw new OfXException.CannotFindHandlerForOfAttribute(attributeType); + + var modelArg = handlerType.GetGenericArguments()[0]; + + var pipeline = serviceProvider + .GetRequiredService(typeof(ReceivedPipelinesImpl<,>).MakeGenericType(modelArg, attributeType)); + + var pipelineMethod = OfXCached.GetPipelineMethodByAttribute(pipeline, attributeType); + + var requestContextType = typeof(RequestContextImpl<>).MakeGenericType(attributeType); + + var queryType = typeof(RequestOf<>).MakeGenericType(attributeType); + + var query = OfXCached.CreateInstanceWithCache(queryType, messageDeserialize.Query.SelectorIds.ToList(), + messageDeserialize.Query.Expression); + var headers = messageDeserialize.Headers; + var requestContext = Activator + .CreateInstance(requestContextType, query, headers, CancellationToken.None); + // Invoke the method and get the result + var response = ((Task>)pipelineMethod! + .Invoke(pipeline, [requestContext]))!; + var result = JsonSerializer.Serialize(response.Result); + connection.Publish(args.Message.Reply, Encoding.UTF8.GetBytes(result)); + })); + } +} \ No newline at end of file diff --git a/src/OfX.Tests/Attributes/UserOfAttribute.cs b/src/OfX.Tests/Attributes/UserOfAttribute.cs index a851a85..fc88997 100644 --- a/src/OfX.Tests/Attributes/UserOfAttribute.cs +++ b/src/OfX.Tests/Attributes/UserOfAttribute.cs @@ -1,4 +1,3 @@ -using OfX.Abstractions; using OfX.Attributes; namespace OfX.Tests.Attributes; diff --git a/src/OfX.Tests/Handlers/UserRequestHandler.cs b/src/OfX.Tests/Handlers/UserRequestHandler.cs index f962f8c..cb6a332 100644 --- a/src/OfX.Tests/Handlers/UserRequestHandler.cs +++ b/src/OfX.Tests/Handlers/UserRequestHandler.cs @@ -1,16 +1,16 @@ -using OfX.Abstractions; -using OfX.Responses; -using OfX.Tests.Attributes; -using OfX.Tests.Models; - -namespace OfX.Tests.Handlers; - -public sealed class UserRequestHandler(IQueryOfHandler userQueryOf) - : IMappableRequestHandler -{ - public async Task> RequestAsync(RequestContext context) - { - var data = await userQueryOf.GetDataAsync(context); - return data; - } -} \ No newline at end of file +// using OfX.Abstractions; +// using OfX.Responses; +// using OfX.Tests.Attributes; +// using OfX.Tests.Models; +// +// namespace OfX.Tests.Handlers; +// +// public sealed class UserRequestHandler(IQueryOfHandler userQueryOf) +// : IMappableRequestHandler +// { +// public async Task> RequestAsync(RequestContext context) +// { +// var data = await userQueryOf.GetDataAsync(context); +// return data; +// } +// } \ No newline at end of file diff --git a/src/OfX.Tests/OfX.Tests.csproj b/src/OfX.Tests/OfX.Tests.csproj index c6a3f13..9f34719 100644 --- a/src/OfX.Tests/OfX.Tests.csproj +++ b/src/OfX.Tests/OfX.Tests.csproj @@ -28,6 +28,7 @@ + diff --git a/src/OfX.Tests/OfXNatsTest.cs b/src/OfX.Tests/OfXNatsTest.cs new file mode 100644 index 0000000..66aa1a4 --- /dev/null +++ b/src/OfX.Tests/OfXNatsTest.cs @@ -0,0 +1,55 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using OfX.Abstractions; +using OfX.EntityFrameworkCore.Extensions; +using OfX.Extensions; +using OfX.Nats.Extensions; +using OfX.Tests.Contexts; +using OfX.Tests.Models; +using Xunit; + +namespace OfX.Tests; + +public sealed class OfXNatsTest : ServicesBuilding +{ + public OfXNatsTest() + { + var assembly = typeof(ITestAssemblyMarker).Assembly; + InstallService((serviceCollection, _) => serviceCollection + .AddDbContext(opts => opts + .UseInMemoryDatabase($"Test_{Guid.NewGuid()}"))) + .InstallService((serviceCollection, _) => + { + serviceCollection.AddOfX(options => + { + options.AddAttributesContainNamespaces(assembly); + options.AddHandlersFromNamespaceContaining(); + options.AddNats(config => config + .UseNats(c => c.Host("nats://localhost:4222"))); + }) + .AddOfXEFCore(options => + { + options.AddDbContexts(typeof(TestDbContext)); + options.AddModelConfigurationsFromNamespaceContaining(); + }); + }) + .InstallAllServices(); + var dbContext = ServiceProvider.GetRequiredService(); + dbContext.Users.AddRange(StaticData.StaticDataTest.Users); + dbContext.SaveChanges(); + ServiceProvider.StartNatsServerAsync(); + } + + [Fact] + public async Task Test() + { + await Task.Yield(); + // var dbContext = ServiceProvider.GetRequiredService(); + // var member = new Member { UserId = "1" }; + // var user = await dbContext.Users.FirstOrDefaultAsync(a => a.Id == "1"); + // var dataMappableService = ServiceProvider.GetRequiredService(); + // await dataMappableService.MapDataAsync(member); + // Assert.Equal(user?.Name, member.UserName); + // Assert.Equal(user?.Email, member.UserEmail); + } +} \ No newline at end of file diff --git a/src/OfX/Abstractions/IContext.cs b/src/OfX/Abstractions/IContext.cs index 929b13d..84a05d8 100644 --- a/src/OfX/Abstractions/IContext.cs +++ b/src/OfX/Abstractions/IContext.cs @@ -1,13 +1,21 @@ using OfX.Attributes; namespace OfX.Abstractions; - +/// +/// IContext is a RequestContext, which is used for each request +/// When you invoke the MapDataAsync function from IMappableService, you can pass the Context to this function, this is optional! +/// Headers: you can send anything to the server, this is a additional data, and you can handle the request as you want! +/// CancellationToken: You can use CancellationToken like-when you want to cancel the request after some seconds, right! +/// public interface IContext { Dictionary Headers { get; } CancellationToken CancellationToken { get; } } - +/// +/// When you received the request, it is wrapped on a request context. Here you can find the header and CancellationToken on the requestContext +/// +/// public interface RequestContext : IContext where TAttribute : OfXAttribute { RequestOf Query { get; } diff --git a/src/OfX/Abstractions/IDataMappableService.cs b/src/OfX/Abstractions/IDataMappableService.cs index 6db7f25..153639d 100644 --- a/src/OfX/Abstractions/IDataMappableService.cs +++ b/src/OfX/Abstractions/IDataMappableService.cs @@ -1,5 +1,8 @@ namespace OfX.Abstractions; +/// +/// This is the abstraction. You can map anything within this function MapDataAsync! +/// public interface IDataMappableService { Task MapDataAsync(object value, IContext context = null); diff --git a/src/OfX/Abstractions/IOfXAttributeCore.cs b/src/OfX/Abstractions/IOfXAttributeCore.cs index 8cd2823..4f8ad7d 100644 --- a/src/OfX/Abstractions/IOfXAttributeCore.cs +++ b/src/OfX/Abstractions/IOfXAttributeCore.cs @@ -1,5 +1,18 @@ namespace OfX.Abstractions; +/// +/// OfXAttributeCore, this is the Core Of OfX Attribute, all the Attribute should have those properties! +/// This is the nameof selector property, we will use this propertyName to locate the selector! +/// PropertyName +/// +/// Use this when you want to get customize data, like Expression="Email" +/// Expression +/// +/// When you want to map data with Order, ex: If the table A has only Id of table X on other service, we have to +/// get this Id first, then we will get ordered data by that Id! +/// Order +/// +/// public interface IOfXAttributeCore { string PropertyName { get; } diff --git a/src/OfX/Abstractions/IQueryOfHandler.cs b/src/OfX/Abstractions/IQueryOfHandler.cs index 2d731b4..4dde09e 100644 --- a/src/OfX/Abstractions/IQueryOfHandler.cs +++ b/src/OfX/Abstractions/IQueryOfHandler.cs @@ -2,7 +2,11 @@ using OfX.Responses; namespace OfX.Abstractions; - +/// +/// IQueryOfHandler: used for server. This abstraction is used to get data from Server, like OfX.EntityFramework +/// +/// +/// public interface IQueryOfHandler where TModel : class where TAttribute : OfXAttribute { diff --git a/src/OfX/Abstractions/IReceivedPipelineBehavior.cs b/src/OfX/Abstractions/IReceivedPipelineBehavior.cs index 0098921..dc7622d 100644 --- a/src/OfX/Abstractions/IReceivedPipelineBehavior.cs +++ b/src/OfX/Abstractions/IReceivedPipelineBehavior.cs @@ -3,6 +3,10 @@ namespace OfX.Abstractions; +/// +/// This is the Abstract layer, is used to create customize pipeline. This is used for-like IQueryOfHandler. +/// +/// public interface IReceivedPipelineBehavior where TTAttribute : OfXAttribute { Task> HandleAsync(RequestContext requestContext, diff --git a/src/OfX/Abstractions/IRequestPipeline.cs b/src/OfX/Abstractions/IRequestPipeline.cs deleted file mode 100644 index 9e1a9d2..0000000 --- a/src/OfX/Abstractions/IRequestPipeline.cs +++ /dev/null @@ -1,9 +0,0 @@ -using OfX.Attributes; -using OfX.Responses; - -namespace OfX.Abstractions; - -public interface IRequestPipeline where TAttribute : OfXAttribute -{ - Task> HandleAsync(RequestContext context); -} \ No newline at end of file diff --git a/src/OfX/Abstractions/RequestOf.cs b/src/OfX/Abstractions/RequestOf.cs index c13be08..aeafe80 100644 --- a/src/OfX/Abstractions/RequestOf.cs +++ b/src/OfX/Abstractions/RequestOf.cs @@ -2,6 +2,12 @@ namespace OfX.Abstractions; +/// +/// We will create the request based on RequestOf! +/// +/// +/// +/// public sealed record RequestOf(List SelectorIds, string Expression) : GetDataMappableQuery(SelectorIds, Expression), IDataMappableOf where TAttribute : Attribute, IDataMappableCore; \ No newline at end of file diff --git a/src/OfX/Cached/OfXCached.cs b/src/OfX/Cached/OfXCached.cs index b8fb742..65df6f8 100644 --- a/src/OfX/Cached/OfXCached.cs +++ b/src/OfX/Cached/OfXCached.cs @@ -1,14 +1,17 @@ using System.Collections.Concurrent; using System.Linq.Expressions; using System.Reflection; -using OfX.ApplicationModels; +using OfX.Abstractions; namespace OfX.Cached; public static class OfXCached { internal static Dictionary InternalQueryMapHandler { get; } = []; - public static IReadOnlyDictionary QueryMapHandler => InternalQueryMapHandler; + public static IReadOnlyDictionary AttributeMapHandler => InternalQueryMapHandler; + + private static readonly Lazy> MethodInfoStorage = + new(() => new ConcurrentDictionary()); private static readonly Lazy>> ConstructorCache = new(() => []); @@ -30,4 +33,10 @@ public static object CreateInstanceWithCache(Type type, params object[] args) return factory(args); } + + public static MethodInfo GetPipelineMethodByAttribute(object pipeline, Type attributeType) => + MethodInfoStorage.Value.GetOrAdd(attributeType, q => pipeline.GetType().GetMethods() + .FirstOrDefault(m => + m.Name == "ExecuteAsync" && m.GetParameters() is { Length: 1 } parameters && + parameters[0].ParameterType == typeof(RequestContext<>).MakeGenericType(q))); } \ No newline at end of file diff --git a/src/OfX.Grpc/HandlersInstaller/DefaultGrpcClientsInstaller.cs b/src/OfX/Clients/ClientsInstaller.cs similarity index 73% rename from src/OfX.Grpc/HandlersInstaller/DefaultGrpcClientsInstaller.cs rename to src/OfX/Clients/ClientsInstaller.cs index ae8ebf9..057aae5 100644 --- a/src/OfX.Grpc/HandlersInstaller/DefaultGrpcClientsInstaller.cs +++ b/src/OfX/Clients/ClientsInstaller.cs @@ -3,23 +3,28 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using OfX.Abstractions; -using OfX.Grpc.Abstractions; -namespace OfX.Grpc.HandlersInstaller; +namespace OfX.Clients; -internal static class DefaultGrpcClientsInstaller +public static class ClientsInstaller { - public static void InstallerServices(IServiceCollection services, params Type[] attributeTypes) + /// + /// ServiceSubstituteType must be implemented from IMappableRequestHandler and have only IServiceProvider! + /// + /// + /// + /// + public static void InstallMappableRequestHandlers(IServiceCollection serviceCollection, Type serviceSubstituteType, params Type[] attributeTypes) { var attributesBuilding = attributeTypes - .Select(a => (AttributeType: a, InterfaceType: typeof(IOfXGrpcRequestClient<>).MakeGenericType(a), + .Select(a => (AttributeType: a, InterfaceType: serviceSubstituteType.MakeGenericType(a), ServiceType: typeof(IMappableRequestHandler<>).MakeGenericType(a))) .ToList(); var assemblyName = new AssemblyName { Name = "DynamicInstanceAssemblyHandlers" }; var newAssembly = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); var newModule = newAssembly.DefineDynamicModule("DynamicInstanceModule"); - var typeBuilder = newModule.DefineType("DefaultGrpcClientOfXHandlers", TypeAttributes.Public, null, + var typeBuilder = newModule.DefineType("DynamicOfXClientHandlers", TypeAttributes.Public, null, attributesBuilding.Select(a => a.InterfaceType).ToArray()); // Add the constructor @@ -49,16 +54,16 @@ public static void InstallerServices(IServiceCollection services, params Type[] var handlersType = typeBuilder.CreateType(); attributesBuilding.ForEach(c => { - var existedService = services.FirstOrDefault(a => a.ServiceType == c.ServiceType); + var existedService = serviceCollection.FirstOrDefault(a => a.ServiceType == c.ServiceType); if (existedService is not null) { if (existedService.ImplementationType != typeof(DefaultMappableRequestHandler<>).MakeGenericType(c.AttributeType)) return; - services.Replace(new ServiceDescriptor(c.ServiceType, handlersType, ServiceLifetime.Scoped)); + serviceCollection.Replace(new ServiceDescriptor(c.ServiceType, handlersType, ServiceLifetime.Scoped)); return; } - services.AddScoped(c.ServiceType, handlersType); + serviceCollection.AddScoped(c.ServiceType, handlersType); }); } } \ No newline at end of file diff --git a/src/OfX/Exceptions/OfXException.cs b/src/OfX/Exceptions/OfXException.cs index abb9559..3d9254a 100644 --- a/src/OfX/Exceptions/OfXException.cs +++ b/src/OfX/Exceptions/OfXException.cs @@ -7,10 +7,13 @@ public sealed class RequestMustNotBeAddMoreThanOneTimes() public sealed class AttributesFromNamespaceShouldBeAdded() : Exception("Attributes from namespaces should be added!"); - + public sealed class CurrentIdTypeWasNotSupported() : Exception("Current Id type was not supported. Please create a join us to contribute more!"); - + public sealed class PipelineIsNotReceivedPipelineBehavior(Type type) : Exception($"The input pipeline: {type.Name} is not matched with ReceivedPipelineBehavior. Please check again!"); + + public sealed class CannotFindHandlerForOfAttribute(Type type) + : Exception($"Cannot find handler for OfXAttribute type: {type.Name}!"); } \ No newline at end of file diff --git a/src/OfX/Extensions/OfXExtensions.cs b/src/OfX/Extensions/OfXExtensions.cs index 84da98d..a27cd16 100644 --- a/src/OfX/Extensions/OfXExtensions.cs +++ b/src/OfX/Extensions/OfXExtensions.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using OfX.Abstractions; -using OfX.Attributes; using OfX.Cached; using OfX.Exceptions; using OfX.Implementations; @@ -47,22 +46,20 @@ public static OfXRegister AddOfX(this IServiceCollection serviceCollection, Acti } var defaultImplementedInterface = typeof(DefaultMappableRequestHandler<>); - newOfRegister.AttributesRegister.SelectMany(a => a.ExportedTypes) - .Where(t => t is { IsClass: true, IsAbstract: false } && typeof(OfXAttribute).IsAssignableFrom(t)) - .ForEach(attributeType => - { - // I have to create a default handler, which is typically return an empty collection. Great! - // So the interface with the default method is a best choice! - var parentType = targetInterface.MakeGenericType(attributeType); - var defaultImplementedService = defaultImplementedInterface.MakeGenericType(attributeType); - // Using TryAddScoped is pretty cool. We don't need to check if the service is register or not! - // So we have to replace the default service if existed -> Good! - serviceCollection.TryAddScoped(parentType, defaultImplementedService); - }); - + newOfRegister.OfXAttributeTypes.ForEach(attributeType => + { + // I have to create a default handler, which is typically return an empty collection. Great! + // So the interface with the default method is a best choice! + var parentType = targetInterface.MakeGenericType(attributeType); + var defaultImplementedService = defaultImplementedInterface.MakeGenericType(attributeType); + // Using TryAddScoped is pretty cool. We don't need to check if the service is register or not! + // So we have to replace the default service if existed -> Good! + serviceCollection.TryAddScoped(parentType, defaultImplementedService); + }); + serviceCollection.AddScoped(sp => new DataMappableService(sp, newOfRegister.AttributesRegister)); - + serviceCollection.AddTransient(typeof(ReceivedPipelinesImpl<,>)); return newOfRegister; diff --git a/src/OfX/OfX.csproj b/src/OfX/OfX.csproj index 51bbda8..2d2cb1a 100644 --- a/src/OfX/OfX.csproj +++ b/src/OfX/OfX.csproj @@ -4,7 +4,7 @@ enable net9.0;net8.0 default - 3.1.1 + 3.1.2 Quy Vu OfX The high performance and easiest way to play with microservices for .NET diff --git a/src/OfX/Registries/OfXRegister.cs b/src/OfX/Registries/OfXRegister.cs index 64a6a5c..d14a128 100644 --- a/src/OfX/Registries/OfXRegister.cs +++ b/src/OfX/Registries/OfXRegister.cs @@ -1,10 +1,12 @@ using System.Reflection; using Microsoft.Extensions.DependencyInjection; +using OfX.Attributes; namespace OfX.Registries; public class OfXRegister(IServiceCollection serviceCollection) { + private static List OfXAttributeTypesCached; public List AttributesRegister { get; private set; } = []; public Assembly HandlersRegister { get; private set; } public IServiceCollection ServiceCollection { get; } = serviceCollection; @@ -14,4 +16,10 @@ public void AddHandlersFromNamespaceContaining() => public void AddAttributesContainNamespaces(params Assembly[] attributeAssemblies) => AttributesRegister = [..attributeAssemblies]; + + public List OfXAttributeTypes => OfXAttributeTypesCached ??= + [ + ..AttributesRegister.SelectMany(a => a.ExportedTypes) + .Where(a => typeof(OfXAttribute).IsAssignableFrom(a) && !a.IsInterface && !a.IsAbstract && a.IsClass) + ]; } \ No newline at end of file