This is a plug-and-play MagicOnion template with generic service and hub implementations that use MemoryPack serialization. Focused on performance and security this template introduces a built-in rate limiter using Redis. This limiter serves as a robust defense against Denial of Service (DoS) attacks and guards against resource depletion. The template also integrates advanced encryption techniques like Diffie-Hellman and AES-GCM to secure end-to-end encryption and effective prevention of token theft. In parallel, it streamlines development by providing standard Create, Read, Update, and Delete (CRUD) operations via the services and hubs components, thereby expediting the development lifecycle.
I appreciate every star ⭐ that my projects receive, and your support means a lot to me! If you find my projects useful or enjoyable, please consider giving them a star.
You can install this template using NuGet:
dotnet new install MagicOnionGenericTemplate
For template help
dotnet new magic-onion-generic-h
By default, the project is created on .NET 7.0 and gRPC connections are configured to use SSL
dotnet new magic-onion-generic -n YourProjectName
Alternatively, you can disable SSL configuration with:
dotnet new magic-onion-generic -n YourProjectName -F net7.0 -S false
[!IMPORTANT] Enviromental Setup If your development environment is on a macos, the ssl configuration will not work due to the lack of ALPN support on mac. See issue here Mac Users should also Comment the below from appsettings.json
"HTTPS": { "Url": "https://localhost:7197", "Protocols": "Http2" },
Before running the project
- Make sure redis server is running on localhost:6379 (Or you can change it from appsettings.json file both in Web & Server Projects)
- Create a new migration and update database but before that in the server project, you must
- Set your connection string in the appsettings.json file
- In program.cs change the below section according to your Database preference, by default the template uses Sql Database but suports MySql and Oracle Databases without any additional configuration.
options => options.UseSqlManager ( builder.Configuration.GetConnectionString(nameof(MagicTContext)), ServerVersion.AutoDetect(builder.Configuration.GetConnectionString(nameof(MagicTContext))) ) );
When using MagicOnion to create a protocol schema, we typically inherit from IService<>. However, with this template, we inherit from IMagicService<> or IMagicSecureService to leverage additional service methods. The service methods available in these interfaces can be found here.
Important: When using IMagicService or IMagicSecureService, it is crucial to inherit accordingly on both the client and server sides.
This distinction is necessary because IMagicSecureService inherits from IMagicService and handles sending the service token to the server. The server needs to be able to read the token. Failing to follow this rule will result in either the inability to send the token from the client side or the inability to read the token on the server side, leading to exceptions.
Client Side:
MagicClientSecureService -> MagicClientService -> MagicClientServiceBase -> IService
AuthorizationFilter: Adds token to MetaData before server calls (MagicOnion Client Filters).
Server Side: MagicServerSecureService -> AuditDatabaseService -> MagicServerService -> DatabaseService -> MagicServerBase
Enough, show me the code!
Step 1
// By inheriting from IMagicService instead of IService, we can utilize the methods implemented in IMagicService which I provided more
// Information
public interface IUserService : IMagicService<IUserService, USERS>
{
}
Step 2
//The [RegisterScoped] attribute is provided by a source generator library that generates boilerplate code for Dependency Injection. You can find a //detailed walkthrough in the documentation.
//I've mentioned this attribute, shared the repository link, and provided more information in the MagicT.Shared section
[RegisterScoped]
public sealed class UserService : MagicClientervice<IUserService, USERS>, IUserService
{
public UserService(IServiceProvider provider) : base(provider)
{
}
}
Step 3
public sealed partial class UserService : MagicServerService<IUserService, USERS, MagicTContext>, IUserService
{
public UserService(IServiceProvider provider) : base(provider)
{
}
}
Now you are ready to inject and call the services!
Admin users can be configured through the appsettings.json file in the server project. The default login information is as follows:
for the WebTemplate project you can login at /admin/Login
Swagger runs on : https://localhost:5028/Swagger/index.html
From this point on, for simplicity, the tutorial will examine what each project is and the technologies used in each project. The projects are:
The Shared project is referenced by other projects and consists of models, interfaces, hub implementations, extension methods, and other helpers.
1. Cysharp/MemoryPack - it provides full support for high performance serialization and deserialization of binary objects
Use Case: Magiconion by default uses messagepack serialization, I've configured to use Memorypack serialization for this project
Repository link: https://github.com/Cysharp/MemoryPack
2. Cysharp/MessagePipe - is a high-performance in-memory/distributed messaging pipeline for .NET and Unity
Use Case: I use this library mainly to notify view, however you can use it like kafka or RabbitMq.
Repository link: https://github.com/Cysharp/MessagePipe
3. Serilog - Logging Library
4. Mapster - Mapper
5. BouncyCastle - .NET implementation of cryptographic algorithms and protocols
Use Case: We will be using this library to provide end to end encryption to our services. Will review this in other sections.
1. AutoRegisterInject
Short Description: C# source generator that will automatically create Microsoft.Extensions.DependencyInjection registrations for types marked with attributes.
Use Case: Generates boilerplate code for DI Registrations
Repository link : https://github.com/patrickklaeren/AutoRegisterInject
2. AutomaticDisposeImpl
Short Description: A source generator that automatically implements methods corresponding to the IDisposable and IAsyncDisposable implementation patterns in C#
Use Case: Generates boilerplate code for object disposal
Repository link : https://github.com/benutomo-dev/RoslynComponents
3. Generator.Equals (Optional)
Short Description: A source code generator for automatically implementing IEquatable<T> using only attributes.
Use Case: Generates boilerplate code for IEquatable<T>, this helps checking if all values of two differet instances of the same class are the same and is not a must use
feature in this project
Repository link: https://github.com/diegofrata/Generator.Equals
4. MapDataReader
Short Description: This source code generator creates mapping code that can be used as an IDbDataReader extension.
It facilitates easy and efficient data mapping from a database reader to your objects.
Use case: Generates boilerplate code for mapping, EFCore is not efficient when executing custom queries and this library comes in handy in those situations, we will come
review this library again in the Server Project section.
Repository link: https://github.com/jitbit/mapdatareader
I have made some extension methods for different use cases, you can find them in the extensions folder.
these methods are both used in web, client and server projects. The code is readable and well-documented, and you can find references to where the methods are called.
Therefore, I don't feel the need to explain what each extension method does.
in the CryptoHelper.cs class we have Encrypt and Decrypt methods that we will be using for end to end encryption using diffie-hellman key exchange.
I have implemented some classes so that when you create and run migrations, they will become tables in the database.
These classes are responsible for storing traces of user actions (audit/history) such when a user performs a CRUD operation, the logs will be stored in these tables.
I have also created classes that hold user data, permissions, and roles. Within these classes, you can assign permissions to roles and roles to users. Additionally, permissions can be independently assigned to the users.
We will review this in the web project where we have views for these tables.
Now, onto the main dish: MagicOnion treats C# interfaces as a protocol schema, enabling seamless code sharing between C# projects without the need for .proto files.
I've inherited from these interfaces and implemented new ones. On both the client and server sides, they provide us with generic boilerplate code for CRUD operations, logging generic queries, streaming, and more. Of course, we can still use regular MagicOnion interfaces alongside the ones we've implemented.
In magiconion this is how we create our service schema
public interface IMyFirstService : IService<IMyFirstService>
{
// The return type must be `UnaryResult<T>` or `UnaryResult`.
UnaryResult<int> SumAsync(int x, int y);
}
When dealing with hundreds of tables, maintaining all these services with CRUD operations and data manipulation methods can be quite painful. That's where generics come to our aid.
We have two types of interfaces for services: IMagicService<TService, TModel> and ISecureMagicService<TService, TModel>.
Both of these services feature the following method signatures:
CreateAsync: Used to create a new instance of the specified model.
FindByParentAsync: Used to retrieve a list of models based on a parent's primary key request.
FindByParametersAsync: Used to retrieve a list of models based on given parameters.
ReadAsync: Used to retrieve all models.
StreamReadAllAsync: Used to retrieve all models in batches.
UpdateAsync: Used to update the specified model.
DeleteAsync: Used to delete the specified model.
ISecureMagicService additionaly contains the following method signatures:
CreateEncrypted: Creates a new instance of the specified model using encrypted data.
ReadEncrypted: Retrieves all models using encrypted data.
UpdateEncrypted: Updates the specified model using encrypted data.
DeleteEncrypted: Deletes the specified model using encrypted data.
FindByParentEncrypted: Retrieves a list of encrypted data items of a specified model type that are associated with a parent.
FindByParametersEncrypted: Retrieves a list of models based on given parameters.
StreamReadAllEncypted: Streams and reads encrypted data items of a specified model type in batches.
Example Implementation:
public interface IUserService : ISecureMagicService<IUserService, USERS>// Where USERS is our DatabaseModel
{
}
or
public interface IUserService : IMagicService<IUserService, USERS>// Where USERS is our DatabaseModel
{
}
In magiconion this is how we create our Hub schema
// Server -> Client definition
public interface IGamingHubReceiver
{
// The method must have a return type of `void` and can have up to 15 parameters of any type.
void OnJoin(Player player);
void OnLeave(Player player);
void OnMove(Player player);
}
// Client -> Server definition
// implements `IStreamingHub<TSelf, TReceiver>` and share this type between server and client.
public interface IGamingHub : IStreamingHub<IGamingHub, IGamingHubReceiver>
{
// The method must return `ValueTask`, `ValueTask<T>`, `Task` or `Task<T>` and can have up to 15 parameters of any type.
ValueTask<Player[]> JoinAsync(string roomName, string userName, Vector3 position, Quaternion rotation);
ValueTask LeaveAsync();
ValueTask MoveAsync(Vector3 position, Quaternion rotation);
}
Instead we will now inherit from IMagicHub<THub, TReceiver, TModel> and IMagicReceiver
Example Implementation:
public interface ITestHub : IMagicHub<ITestHub, ITestHubReceiver, TestModel>
{
}
public interface ITestHubReceiver : IMagicReceiver<TestModel>
{
}
That's all with MagicT.Shared Section!
This project is referenced by the Client and Server project and includes services for rate limiting, token caching, and IP blocking. However, the key class in this project is MagicTRedisDatabase. This class initializes a new instance of the MagicTRedisDatabase class with the specified configuration and includes the following methods:
Create
AddOrUpdate
ReadAs
Update
Delete
Push
PullAs
This project implements base classes that inherit from the Components base and are responsible for handling CRUD requests from the view and projecting exceptions/errors onto the view. There are four base classes that our Razor pages will inherit from: PageBase, ServicePageBase, SecuredServicePageBase, and HubPageBase. Additionally, this project includes some TaskExtensions to better manage service calls.
is an abstract class designed to serve as a base class for Blazor components. It integrates several key services and provides methods to handle asynchronous and synchronous tasks with error handling and notifications. Here is a summary of its main functionalities:
The OnInitializedAsync method initializes the notifications view and calls the OnBeforeInitializeAsync method, which can be overridden by derived classes to perform additional initialization tasks. Task Execution with Error Handling:
This class streamlines the integration of common services and error handling for Blazor components, promoting code reuse and consistency across the application.
The ServicePageBase class provides a base implementation for Blazor pages that interact with a service to manage data models. It extends the PageBaseClass and adds functionality for handling CRUD (Create, Read, Update, Delete) operations and file uploads. The class supports generic data models and services, allowing for flexible use with different types of data. Here's a detailed breakdown:
TChild: The type of the child model related to the parent model.
ParentModel: The parent data model.
Overrides OnBeforeInitializeAsync to also call FindByParentAsync.
LoadAsync: Validates the parent model before proceeding with view loading.
CreateAsync: Ensures the child model is related to the parent model.
FindByParentAsync: Finds child models related to the parent model and updates the data source.
The HubPageBase class provides a base implementation for Blazor pages that interact with SignalR hubs to manage data models. This class, which extends PageBaseClass, facilitates real-time data operations through SignalR hubs and includes support for CRUD operations, error handling, and data synchronization. Here's a detailed breakdown:
CreateAsync: Ensures the child model is related to the parent model by setting the foreign key. FindByParentAsync: Finds child models related to the parent model via the hub service and updates the data source.
The TaskExtensions class provides extension methods for handling the completion of asynchronous tasks in C#. These methods allow you to execute additional actions or functions before and after the task completes, making it easier to manage task results and handle errors consistently. Here's a breakdown of each method:
OnComplete
OnComplete
OnComplete<T, TArg>(this Task
OnComplete
Common Behavior Task Result Handling: Each method captures the task result and determines its status (success or fail). If the task completes successfully and returns a non-null result, the status is TaskResult.Success. If the result is null or an exception occurs, the status is TaskResult.Fail.
Action/Function Invocation: The specified action or function is invoked with the task result and status. This allows you to handle task completion logic in a consistent manner.
Return Value: The methods return the original task result, allowing you to continue using the result in your application logic.
These two web projects both reference the MagicT.Shared and MagicT.Web.Shared projects. The difference is that MagicT.Web is a Blazor Server assembly project, while MagicT.WebTemplate is an MVC Core project configured to render Blazor pages. I love Blazor, but for me, one of the biggest disadvantages is its incompatibility with some jQuery libraries. When using template pages that depend on jQuery, they do not function well. In these cases, I use the MVC Core project to apply the template and use the services in Blazor components so that I can have template UI with full functionality.
[!Note] When running the WebTemplate project go to /Login page to switch to Blazor
The Client project implements classes that make calls to the API, and the server project implements endpoint services.
1. LocalStorage - Gives access to browsers local storage
Use Case: Storing UserCredentials etc.
Repository link: https://github.com/Blazored/LocalStorage
1. AQueryMaker - This is another library of mine, It simplifies Database Operations
Use Case: Executing custom queries
Repository link: https://github.com/licentia88/AQueryMaker
2. Coravel - Simplifies task/job scheduling, queuing, caching
Use Case: Queuing background tasks
Repository link: https://github.com/jamesmh/coravel
3. LitJwt - Another Cysharp framework
Use Case: Create LightWeight Tokens
Repository link: https://github.com/jamesmh/coravel
Fork the repository
Create a new branch (git checkout -b feature-branch)
Commit your changes (git commit -m 'Add some feature')
Push to the branch (git push origin feature-branch)
Open a pull request
License This project is licensed under the MIT License