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 @@
enablenet9.0;net8.0default
- 3.1.1
+ 3.1.2Quy VuOfX-EFCoreOfX 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 @@
enablenet9.0;net8.0default
- 3.1.1
+ 3.1.2Quy VuOfX-gRPCOfX 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
- enablenet9.0;net8.0default
- 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/OfXOfX.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 @@
enablenet9.0;net8.0default
- 3.1.1
+ 3.1.2Quy VuOfXThe 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