A Hack System based on ASP.NET Core and Blazor WebAssembly.
Design and implement your program and Execute it in Hack System!
- Install .Net 6.0 SDK.
- Download source code and open in Visual Studio 2019 (16.8+).
- Set
HackSystem.WebHost
andHackSystem.WebAPI
as startup projects. - Press F5 to run.
- Navigate to https://localhost:2473
- Enjoy it.
- Edit
hosting.json
file ofHackSystem.WebHost
andHackSystem.WebAPI
projects to config the port to listen. - Edit
wwwroot/appsettings.json
file ofHackSystem.Web
so thatAPIConfiguration.APIURL
equalsurls
ofHackSystem.WebHost
to config the address of Web API. - Edit
JwtConfiguration
section ofHackSystem.WebAPI
project'sappsettings.json
, it is Important for security! - Publish
HackSystem.WebHost
andHackSystem.WebAPI
projects.HackSystem.WebHost
is just a fake host of core projectHackSystem.Web
.
- Navigate to Hack System:
- Open browser and navigate to the address which you just configured for
HackSystem.WebHost
. - Or you can use the
HackSystem.Host
project to visit Hack System.- Before you launch
HackSystem.Host
, editHostConfigs.json
file so thatRemoteURL
equals the address which you just configured forHackSystem.WebHost
. - Just run
HackSystem.Host.exe
.
- Before you launch
- Open browser and navigate to the address which you just configured for
- Enjoy it.
Something may change with platform developing.
-
Create a new Razor Class Library project.
-
Install above nuget packages to this project.
-
Add a new image file named as Index.png in root folder of this project, and copy to output directory if newer.
-
Create a new Razor Component as entry component, and inherits ProgramComponentBase class.
- Design and Implement it.
-
Create a new static Launcher class and return the type of entry component from static Launch method.
-
public static class Launcher { // Launch Parameter is not mandatory. public static Type Launch(LaunchParameter parameter) { return typeof(TaskSchedulerComponent); } }
-
Something may change with platform developing.
-
Insert a new record in database of new program.
-
INSERT INTO ProgramDetails (Id,Name,Enabled,SingleInstance,EntryAssemblyName,EntryTypeName,EntryParameter,Mandatory) VALUES ('program0-icon-0828-hack-system000006','TaskServer',1,1,'HackSystem.Web.TaskSchedule','HackSystem.Web.TaskSchedule.Launcher','{ "Developer": "Leon" }',1);
-
-
Edit
ProgramAssetConfiguration
section ofHackSystem.WebAPI
project'sappsettings.json
to config program asset folder path. -
Create new folder named as new program ID in program asset folder.
-
Build program project and copy all files into above folder.
-
Post-build event to copy program files into WebAPI's out directory automatically.
set assetFolder=$(SolutionDir)HackSystem.WebAPI\$(OutDir)ProgramAssets\ MKDIR assetFolder set assetFolder=%assetFolder%program0-icon-0828-hack-system000006\ MKDIR assetFolder XCOPY $(TargetDir)* %assetFolder% /Y /S /H
-
-
Insert a new record in database to map releationship between user and program.
-
INSERT INTO UserProgramMaps (UserId,ProgramId,PinToDesktop,PinToDock,PinToTop,"Rename") VALUES ('msaspnet-core-user-hack-system000001','program0-icon-0828-hack-system000006',1,0,0,NULL);
-
-
Launch Hack System and login above user, should see the new program launch and enjoy it.
Click image below to watch video.
Contains all Front-end related projects, such as interfaces or implementations of UI Components, Authentication, Front-end Contracts, Cookie Storage, Domain Models, and a WebHost process.
The core project is HackSystem.Web
, configure Front-end requests process pipe and Dependency injection in Program.cs
, configure Front-end related application settings in wwwroot\appsettings.json
.
Program schedule component of Front-end, used to Lazy load program assemblies, Manage and Schedule Program UI Windows, Launch and Destroy Processes, Produce and Consume programs or processes related notifications.
A development kit used to help developer to design and implement mini programs which can be executed in Hack System.
Mini programs which implemented by above Development Kit. Includes all internal programs of Hack System here:
This is the most useful program for Hack System currently, used to manage and manually trigger Back-end tasks in this program.
Contains all Back-end related projects, such as interfaces or implementations of Back-end Services, Authentication, Back-end Contracts, Domain Models. It works as both of a Web MVC and a Web API programs.
The core project is HackSystem.WebAPI
, configure Back-end requests process pipe, Dependency injection and Security policies in Program.cs
, configure Back-end related application settings in appsettings.json
.
Mock server related interfaces, domain models, implementations.
Mock server related interfaces, domain models, implementations. Used to store and resolve program assets, such as JS, CSS, Assemblies and so on.
Task server related interfaces, domain models, implementations. Used to manage and schedule Back-end Tasks.
Implement and inject Hack System's Tasks in HackSystem.WebAPI.Tasks
project.
Shared contracts or asset for both of Front-end and Back-end.
As you see, Unit Tests.
Declare log format and out level in NLog.config
, log with different level will be wrote into specified log files at Back-end side.
Split different component into sub domains, and create Application, Domain, Infrastructure projects for each sub domain, then register interfaces and implementations in root project of current sub domain. You can find this practice in almost every component of Hack System.
Define interfaces of each service, and use this project as a Abstraction, and can be referred by other projects without any implementation included.
Similar with Application, but this project used to define Domain Models, it's also a Abstraction and can be referred by other projects with any implementation included.
Actual implementation of each sub domain, it should not be referred by other projects directly.
This is a link between application and implementation, root project of each sub domain should contain a static Extension class with a static Extension method on IServiceCollection to inject dependency into DI container, like below:
public static IServiceCollection AttachTaskServer(
this IServiceCollection services,
TaskServerOptions configuration)
{
services
.Configure(new Action<TaskServerOptions>(options => options.TaskServerHost = configuration.TaskServerHost))
.AddSingleton<IHackSystemTaskServer, HackSystemTaskServer>()
.AddScoped<ITaskRepository, TaskRepository>()
.AddScoped<ITaskLogRepository, TaskLogRepository>()
.AddScoped<ITaskLoader, TaskLoader>()
.AddScoped<ITaskScheduleWrapper, TaskScheduleWrapper>()
.AddTransient<ITaskGenericJob, TaskGenericJob>()
.AttachTaskServerInfrastructure()
.AddWebAPITasks();
return services;
}
Currently, we are using EF Core code-first mode in Hack System, which means that there is no need to write any line of SQL, use entity class and linq is enough for all database access scenarios.
Create entity class in HackSystem.WebAPI.Domain\Entity
folder, and define required properties of this entity type.
public class GenericOption
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int OptionID { get; set; }
public string OptionName { get; set; }
public string OptionValue { get; set; }
public string? Category { get; set; }
public string? OwnerLevel { get; set; }
public DateTime CreateTime { get; set; }
public DateTime ModifyTime { get; set; }
}
Add new DbSet<TEntity>
collection property in HackSystem.WebAPI.Infrastructure\DBContexts\HackSystemDbContext.cs
to map to a table in database.
public virtual DbSet<GenericOption> GenericOptions { get; set; }
Design indexes or relationships of each entity type in OnModelCreating
method of HackSystemDbContext.cs
.
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<UserProgramMap>().HasOne(map => map.Program).WithMany(program => program.UserProgramMaps).HasForeignKey(map => map.ProgramId).OnDelete(DeleteBehavior.Cascade);
builder.Entity<UserProgramMap>().HasOne(map => map.ProgramUser).WithMany(user => user.UserProgramMaps).HasForeignKey(map => map.UserId).OnDelete(DeleteBehavior.Cascade);
builder.Entity<UserProgramMap>().HasKey(map => new { map.UserId, map.ProgramId });
builder.Entity<GenericOption>().HasIndex(nameof(GenericOption.OptionName), nameof(GenericOption.Category), nameof(GenericOption.OwnerLevel)).IsUnique();
builder.Entity<GenericOption>().Property(nameof(GenericOption.OptionName)).UseCollation("NOCASE");
builder.Entity<GenericOption>().Property(nameof(GenericOption.Category)).UseCollation("NOCASE");
builder.Entity<GenericOption>().Property(nameof(GenericOption.OwnerLevel)).UseCollation("NOCASE");
}
- Select
HackSystem.WebAPI
as Startup project; - Open
Package Manager Console
in Visual Studio; - Select
HackSystem.WebAPI.Infrastructure
as Default project in Package Manager Console; - Input below commands and execute;
Command | Description |
---|---|
Get-Help entityframework | Displays information about entity framework commands. |
Add-Migration | Creates a migration by adding a migration snapshot. |
Remove-Migration | Removes the last migration snapshot. |
Update-Database | Updates the database schema based on the last migration snapshot. |
Script-Migration | Generates a SQL script using all the migration snapshots. |
Scaffold-DbContext | Generates a DbContext and entity type classes for a specified database. This is called reverse engineering. |
Get-DbContext | Gets information about a DbContext type. |
Drop-Database | Drops the database. |
For examples:
-- To generate database migration code files automatically
Add-Migration AddGenericOptionEntity
-- To execute database migration code files
-- and apply modifications to current connected database file
Update-Database
In above step, it's a development-time manual operation to apply modification to database file.
Here is a way to apply these modification all automatically during production-time, this way is "doing nothing manually", as there is already a function at Back-end side to check pending database schema modification and apply them when launch Back-end program: HackSystem.WebAPI.Infrastructure\DataSeed\DatabaseInitializer.cs
.
private async static Task InitializeDatabaseAsync(IHost host)
{
using var scope = host.Services.CreateScope();
var services = scope.ServiceProvider;
var logger = services.GetRequiredService<ILogger<IHost>>();
var dbContext = services.GetRequiredService<HackSystemDbContext>();
try
{
logger.LogDebug($"Ensure database created...");
await dbContext.Database.EnsureCreatedAsync();
logger.LogDebug($"Check database pending migrations...");
var pendingMigrations = await dbContext.Database.GetPendingMigrationsAsync();
if (pendingMigrations.Any())
{
logger.LogInformation($"Pending database migration should be combined: \n\t{string.Join(",", pendingMigrations)}");
await dbContext.Database.MigrateAsync();
}
logger.LogDebug($"Database check finished.");
}
catch (Exception ex)
{
logger.LogError(ex, $"Database check failed.");
}
}
Define repository interface in HackSystem.WebAPI.Application\Repository
folder.
public interface IGenericOptionRepository : IRepositoryBase<GenericOption>
{
Task<GenericOption> QueryGenericOption(string optionName, string owner = null, string category = null);
}
Implement repository service in HackSystem.WebAPI.Infrastructure\Repository
folder.
public class GenericOptionRepository : RepositoryBase<GenericOption>, IGenericOptionRepository
{
public GenericOptionRepository(
ILogger<GenericOptionRepository> logger,
DbContext dbContext)
: base(logger, dbContext)
{
}
public async Task<GenericOption> QueryGenericOption(string optionName, string owner = null, string category = null)
=> await this
.AsQueryable()
.Where(o =>
(o.OptionName == optionName) &&
(string.IsNullOrEmpty(o.OwnerLevel) || o.OwnerLevel == owner) &&
(string.IsNullOrEmpty(o.Category) || o.Category == category))
.OrderByDescending(o => o.OwnerLevel)
.ThenByDescending(o => o.Category)
.FirstOrDefaultAsync();
}
Inject repository interface and implementation at HackSystem.WebAPI\Extensions\HackSystemInfrastructureExtension.cs
public static IServiceCollection AddHackSystemWebAPIServices(
this IServiceCollection services)
{
services
.AddScoped<IGenericOptionRepository, GenericOptionRepository>();
return services;
}
We use Intermediary
to communicate between components independently, it support multiple kinds of Communication Behaviour:
-
Request
-
IIntermediaryRequest<out TResponse>
-
Each request require to return a response from handler
-
Support only one kind of Request Handler for each Request type
-
-
Command
-
IIntermediaryCommand
-
Each commend require to be processed be handler but without any response.
-
Support only one kind of Command Handler for each Command type
-
-
Notification
-
IIntermediaryNotification
-
Similar with Command mode
-
Support multiple Notifiaction Handlers for each Notification type
-
-
Event
-
IIntermediaryEvent
-
Similar with combination of Command and Notification
-
Multiple Event Handlers point to the same instance reference for each Event type, to process published event.
-
-
IIntermediaryRequestHandler<in TRequest, TResponse>
-
IIntermediaryCommandHandler<TCommand>
-
IIntermediaryNotificationHandler<in TNotification>
-
IIntermediaryEventHandler<TEvent>
-
IIntermediaryPublisher
-
<TResponse> SendRequest<TResponse>(IIntermediaryRequest<TResponse> request, CancellationToken cancellationToken = default)
-
SendCommand(IIntermediaryCommand command, CancellationToken cancellationToken = default)
-
PublishNotification(IIntermediaryNotification notification, CancellationToken cancellationToken = default)
-
PublishEvent(IIntermediaryEvent eventArg, CancellationToken cancellationToken = default)
-