零度框架 8.0 版本

This commit is contained in:
hello 2023-12-05 17:22:48 +08:00
parent e75c3b613c
commit 9781a3b700
853 changed files with 92221 additions and 0 deletions

63
.gitattributes vendored Normal file
View File

@ -0,0 +1,63 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain

363
.gitignore vendored Normal file
View File

@ -0,0 +1,363 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd

View File

@ -0,0 +1,6 @@
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
var app = builder.Build();
app.MapReverseProxy();
app.Run();

View File

@ -0,0 +1,28 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:26394",
"sslPort": 44338
}
},
"profiles": {
"ZeroFramework.ReverseProxy": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7117;http://localhost:5117",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Yarp.ReverseProxy" Version="2.1.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -0,0 +1,49 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ReverseProxy": {
"Routes": {
"identityServer": {
"ClusterId": "identityServer",
"Match": {
"Path": "api/identity/{**remainder}"
},
"Transforms": [
{ "PathPattern": "api/{**remainder}" }
]
},
"deviceCenter": {
"ClusterId": "deviceCenter",
"Match": {
"Path": "api/device/{**remainder}"
},
"Transforms": [
{ "PathPattern": "api/{**remainder}" }
]
}
},
"Clusters": {
"identityServer": {
"LoadBalancingPolicy": "Random",
"Destinations": {
"identityServer.server1": {
"Address": "https://localhost:5001"
}
}
},
"deviceCenter": {
"LoadBalancingPolicy": "Random",
"Destinations": {
"deviceCenter.server1": {
"Address": "https://localhost:6001"
}
}
}
}
}
}

View File

@ -0,0 +1,80 @@
using System.Dynamic;
using System.Text.Json;
using ZeroFramework.EventBus.Abstractions;
using ZeroFramework.EventBus.Events;
namespace ZeroFramework.EventBus.MemoryQueue
{
public class InMemoryEventBus(IEventBusSubscriptionsManager subsManager, IServiceProvider serviceProvider) : IEventBus
{
private readonly IEventBusSubscriptionsManager _subsManager = subsManager;
private readonly IServiceProvider _serviceProvider = serviceProvider;
public async Task PublishAsync(IntegrationEvent @event, CancellationToken cancellationToken = default)
{
string eventName = @event.GetType().Name;
string message = JsonSerializer.Serialize(@event, @event.GetType());
await ProcessEvent(eventName, message);
}
public void Subscribe<T, TH>() where T : IntegrationEvent where TH : IIntegrationEventHandler<T>
{
_subsManager.AddSubscription<T, TH>();
}
public void SubscribeDynamic<TH>(string eventName) where TH : IDynamicIntegrationEventHandler
{
_subsManager.AddDynamicSubscription<TH>(eventName);
}
public void Unsubscribe<T, TH>() where T : IntegrationEvent where TH : IIntegrationEventHandler<T>
{
_subsManager.RemoveSubscription<T, TH>();
}
public void UnsubscribeDynamic<TH>(string eventName) where TH : IDynamicIntegrationEventHandler
{
_subsManager.RemoveDynamicSubscription<TH>(eventName);
}
private async Task ProcessEvent(string eventName, string message)
{
if (_subsManager.HasSubscriptionsForEvent(eventName))
{
var subscriptions = _subsManager.GetHandlersForEvent(eventName);
foreach (var subscription in subscriptions)
{
if (subscription.IsDynamic)
{
if (_serviceProvider.GetService(subscription.HandlerType) is IDynamicIntegrationEventHandler handler)
{
dynamic? eventData = JsonSerializer.Deserialize<ExpandoObject>(message);
await handler.HandleAsync(eventData);
}
}
else
{
var handler = _serviceProvider.GetService(subscription.HandlerType);
if (handler is not null)
{
var eventType = _subsManager.GetEventTypeByName(eventName);
object? integrationEvent = JsonSerializer.Deserialize(message, eventType);
var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
if (integrationEvent is not null)
{
Task? task = concreteType.GetMethod("HandleAsync")?.Invoke(handler, new object[] { integrationEvent }) as Task;
task ??= Task.CompletedTask;
await task;
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ZeroFramework.EventBus\ZeroFramework.EventBus.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,138 @@
using Microsoft.Extensions.Logging;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using RabbitMQ.Client.Exceptions;
using System.Net.Sockets;
namespace ZeroFramework.EventBus.RabbitMQ
{
public class DefaultRabbitMQPersistentConnection(IConnectionFactory connectionFactory, ILogger<DefaultRabbitMQPersistentConnection> logger, int retryCount = 5) : IRabbitMQPersistentConnection
{
private readonly IConnectionFactory _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
private readonly ILogger<DefaultRabbitMQPersistentConnection> _logger = logger ?? throw new ArgumentNullException(nameof(logger));
private readonly int _retryCount = retryCount;
private IConnection? _connection = null;
readonly object _syncRoot = new();
public bool IsConnected => _connection != null && _connection.IsOpen && !_disposed;
public IModel CreateModel()
{
if (!IsConnected || _connection is null)
{
throw new InvalidOperationException("No RabbitMQ connections are available to perform this action");
}
return _connection.CreateModel();
}
public bool TryConnect()
{
_logger.LogInformation("RabbitMQ Client is trying to connect");
lock (_syncRoot)
{
for (int retryAttempt = 1; retryAttempt <= _retryCount; retryAttempt++)
{
var time = TimeSpan.FromSeconds(Math.Pow(2, retryAttempt));
try
{
_connection = _connectionFactory.CreateConnection();
break;
}
catch (SystemException ex) when (ex is BrokerUnreachableException || ex is SocketException)
{
_logger.LogWarning(ex, "RabbitMQ Client could not connect after {TimeOut}s ({ExceptionMessage})", $"{time.TotalSeconds:n1}", ex.Message);
}
Task.Delay(time).Wait();
}
if (IsConnected && _connection is not null)
{
_connection.ConnectionShutdown += OnConnectionShutdown!;
_connection.CallbackException += OnCallbackException!;
_connection.ConnectionBlocked += OnConnectionBlocked!;
_logger.LogInformation("RabbitMQ Client acquired a persistent connection to '{HostName}' and is subscribed to failure events", _connection.Endpoint.HostName);
return true;
}
else
{
_logger.LogCritical("FATAL ERROR: RabbitMQ connections could not be created and opened");
return false;
}
}
}
private void OnConnectionBlocked(object sender, ConnectionBlockedEventArgs e)
{
if (_disposed) return;
_logger.LogWarning("A RabbitMQ connection is shutdown. Trying to re-connect...");
TryConnect();
}
void OnCallbackException(object sender, CallbackExceptionEventArgs e)
{
if (_disposed) return;
_logger.LogWarning("A RabbitMQ connection throw exception. Trying to re-connect...");
TryConnect();
}
void OnConnectionShutdown(object sender, ShutdownEventArgs reason)
{
if (_disposed) return;
_logger.LogWarning("A RabbitMQ connection is on shutdown. Trying to re-connect...");
TryConnect();
}
private bool _disposed;
// Protected implementation of Dispose pattern.
//https://docs.microsoft.com/zh-cn/dotnet/standard/garbage-collection/implementing-dispose
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
// Release any managed resources here.
if (disposing)
{
// dispose managed state (managed objects).
_connection?.Dispose();
}
// free unmanaged resources (unmanaged objects) and override a finalizer below.
// set large fields to null.
_disposed = true;
// Call the base class implementation.
//base.Dispose(disposing);
}
// Public implementation of Dispose pattern callable by consumers.
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
~DefaultRabbitMQPersistentConnection() => Dispose(false);
}
}

View File

@ -0,0 +1,300 @@
using Microsoft.Extensions.Logging;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using RabbitMQ.Client.Exceptions;
using System.Dynamic;
using System.Net.Sockets;
using System.Text;
using System.Text.Json;
using ZeroFramework.EventBus.Abstractions;
using ZeroFramework.EventBus.Events;
using ZeroFramework.EventBus.Extensions;
namespace ZeroFramework.EventBus.RabbitMQ
{
public class EventBusRabbitMQ : IEventBus, IDisposable
{
const string ExchangeName = "my_event_bus";
private readonly IRabbitMQPersistentConnection _persistentConnection;
private readonly ILogger<EventBusRabbitMQ> _logger;
private readonly IEventBusSubscriptionsManager _subsManager;
private readonly IServiceProvider _serviceProvider;
private readonly int _retryCount;
private IModel _consumerChannel;
private string? _queueName;
public EventBusRabbitMQ(IRabbitMQPersistentConnection persistentConnection, ILogger<EventBusRabbitMQ> logger, IServiceProvider serviceProvider, IEventBusSubscriptionsManager subsManager, string? queueName, int retryCount = 5)
{
_persistentConnection = persistentConnection ?? throw new ArgumentNullException(nameof(persistentConnection));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_subsManager = subsManager ?? new InMemoryEventBusSubscriptionsManager();
_queueName = queueName;
_consumerChannel = CreateConsumerChannel();
_serviceProvider = serviceProvider;
_retryCount = retryCount;
_subsManager.OnEventRemoved += SubsManager_OnEventRemoved!;
}
private void SubsManager_OnEventRemoved(object sender, string eventName)
{
if (!_persistentConnection.IsConnected)
{
_persistentConnection.TryConnect();
}
using var channel = _persistentConnection.CreateModel();
channel.QueueUnbind(queue: _queueName, exchange: ExchangeName, routingKey: eventName);
if (_subsManager.IsEmpty)
{
_queueName = string.Empty;
_consumerChannel.Close();
}
}
public Task PublishAsync(IntegrationEvent @event, CancellationToken cancellationToken = default)
{
if (!_persistentConnection.IsConnected)
{
_persistentConnection.TryConnect();
}
var eventName = @event.GetType().Name;
_logger.LogTrace("Creating RabbitMQ channel to publish event: {EventId} ({EventName})", @event.Id, eventName);
using var channel = _persistentConnection.CreateModel();
_logger.LogTrace("Declaring RabbitMQ exchange to publish event: {EventId}", @event.Id);
channel.ExchangeDeclare(exchange: ExchangeName, type: ExchangeType.Direct);
var message = JsonSerializer.Serialize(@event, @event.GetType());
var body = Encoding.UTF8.GetBytes(message);
for (int retryAttempt = 1; retryAttempt <= _retryCount; retryAttempt++)
{
var time = TimeSpan.FromSeconds(Math.Pow(2, retryAttempt));
try
{
var properties = channel.CreateBasicProperties();
properties.DeliveryMode = 2; // persistent
_logger.LogTrace("Publishing event to RabbitMQ: {EventId}", @event.Id);
channel.BasicPublish(exchange: ExchangeName, routingKey: eventName, mandatory: true, basicProperties: properties, body: body);
break;
}
catch (SystemException ex) when (ex is BrokerUnreachableException || ex is SocketException)
{
_logger.LogWarning(ex, "Could not publish event: {EventId} after {Timeout}s ({ExceptionMessage})", @event.Id, $"{time.TotalSeconds:n1}", ex.Message);
}
Task.Delay(time, cancellationToken).Wait(cancellationToken);
}
return Task.CompletedTask;
}
public void SubscribeDynamic<TH>(string eventName) where TH : IDynamicIntegrationEventHandler
{
_logger.LogInformation("Subscribing to dynamic event {EventName} with {EventHandler}", eventName, typeof(TH).GetGenericTypeName());
DoInternalSubscription(eventName);
_subsManager.AddDynamicSubscription<TH>(eventName);
StartBasicConsume();
}
public void Subscribe<T, TH>() where T : IntegrationEvent where TH : IIntegrationEventHandler<T>
{
var eventName = _subsManager.GetEventKey<T>();
DoInternalSubscription(eventName);
_logger.LogInformation("Subscribing to event {EventName} with {EventHandler}", eventName, typeof(TH).GetGenericTypeName());
_subsManager.AddSubscription<T, TH>();
StartBasicConsume();
}
private void DoInternalSubscription(string eventName)
{
var containsKey = _subsManager.HasSubscriptionsForEvent(eventName);
if (!containsKey)
{
if (!_persistentConnection.IsConnected)
{
_persistentConnection.TryConnect();
}
using var channel = _persistentConnection.CreateModel();
channel.QueueBind(queue: _queueName, exchange: ExchangeName, routingKey: eventName);
}
}
public void Unsubscribe<T, TH>() where T : IntegrationEvent where TH : IIntegrationEventHandler<T>
{
var eventName = _subsManager.GetEventKey<T>();
_logger.LogInformation("Unsubscribing from event {EventName}", eventName);
_subsManager.RemoveSubscription<T, TH>();
}
public void UnsubscribeDynamic<TH>(string eventName) where TH : IDynamicIntegrationEventHandler
{
_subsManager.RemoveDynamicSubscription<TH>(eventName);
}
private bool disposed = false;
// Protected implementation of Dispose pattern.
protected virtual void Dispose(bool disposing)
{
if (disposed)
{
return;
}
// Release any managed resources here.
if (disposing)
{
// dispose managed state (managed objects).
_consumerChannel?.Dispose();
_subsManager.Clear();
}
// free unmanaged resources (unmanaged objects) and override a finalizer below.
// set large fields to null.
disposed = true;
// Call the base class implementation.
//base.Dispose(disposing);
}
// Public implementation of Dispose pattern callable by consumers.
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
~EventBusRabbitMQ() => Dispose(false);
private void StartBasicConsume()
{
_logger.LogTrace("Starting RabbitMQ basic consume");
if (_consumerChannel != null)
{
var consumer = new AsyncEventingBasicConsumer(_consumerChannel);
consumer.Received += Consumer_Received;
_consumerChannel.BasicConsume(queue: _queueName, autoAck: false, consumer: consumer);
}
else
{
_logger.LogError("StartBasicConsume can't call on _consumerChannel == null");
}
}
private async Task Consumer_Received(object sender, BasicDeliverEventArgs eventArgs)
{
var eventName = eventArgs.RoutingKey;
var message = Encoding.UTF8.GetString(eventArgs.Body.ToArray());
try
{
if (message.ToLowerInvariant().Contains("throw-fake-exception"))
{
throw new InvalidOperationException($"Fake exception requested: \"{message}\"");
}
await ProcessEvent(eventName, message);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "----- ERROR Processing message \"{Message}\"", message);
}
// Even on exception we take the message off the queue.
// in a REAL WORLD app this should be handled with a Dead Letter Exchange (DLX).
// For more information see: https://www.rabbitmq.com/dlx.html
_consumerChannel.BasicAck(eventArgs.DeliveryTag, multiple: false);
}
private IModel CreateConsumerChannel()
{
if (!_persistentConnection.IsConnected)
{
_persistentConnection.TryConnect();
}
_logger.LogTrace("Creating RabbitMQ consumer channel");
var channel = _persistentConnection.CreateModel();
channel.ExchangeDeclare(exchange: ExchangeName, type: ExchangeType.Direct);
channel.QueueDeclare(queue: _queueName, durable: true, exclusive: false, autoDelete: false, arguments: null);
channel.CallbackException += (sender, ea) =>
{
_logger.LogWarning(ea.Exception, "Recreating RabbitMQ consumer channel");
_consumerChannel.Dispose();
_consumerChannel = CreateConsumerChannel();
StartBasicConsume();
};
return channel;
}
private async Task ProcessEvent(string eventName, string message)
{
_logger.LogTrace("Processing RabbitMQ event: {EventName}", eventName);
if (_subsManager.HasSubscriptionsForEvent(eventName))
{
var subscriptions = _subsManager.GetHandlersForEvent(eventName);
foreach (var subscription in subscriptions)
{
if (subscription.IsDynamic)
{
if (_serviceProvider.GetService(subscription.HandlerType) is IDynamicIntegrationEventHandler handler)
{
dynamic? eventData = JsonSerializer.Deserialize<ExpandoObject>(message);
await handler.HandleAsync(eventData);
}
}
else
{
var handler = _serviceProvider.GetService(subscription.HandlerType);
if (handler is not null)
{
var eventType = _subsManager.GetEventTypeByName(eventName);
object? integrationEvent = JsonSerializer.Deserialize(message, eventType);
var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
if (integrationEvent is not null)
{
Task? task = concreteType.GetMethod("HandleAsync")?.Invoke(handler, new object[] { integrationEvent }) as Task;
task ??= Task.CompletedTask;
await task;
}
}
}
}
}
else
{
_logger.LogWarning("No subscription for RabbitMQ event: {EventName}", eventName);
}
}
}
}

View File

@ -0,0 +1,13 @@
using RabbitMQ.Client;
namespace ZeroFramework.EventBus.RabbitMQ
{
public interface IRabbitMQPersistentConnection : IDisposable
{
bool IsConnected { get; }
bool TryConnect();
IModel CreateModel();
}
}

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RabbitMQ.Client" Version="6.7.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ZeroFramework.EventBus\ZeroFramework.EventBus.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,7 @@
namespace ZeroFramework.EventBus.Abstractions
{
public interface IDynamicIntegrationEventHandler : IIntegrationEventHandler
{
Task HandleAsync(dynamic eventData);
}
}

View File

@ -0,0 +1,17 @@
using ZeroFramework.EventBus.Events;
namespace ZeroFramework.EventBus.Abstractions
{
public interface IEventBus
{
Task PublishAsync(IntegrationEvent @event, CancellationToken cancellationToken = default);
void Subscribe<T, TH>() where T : IntegrationEvent where TH : IIntegrationEventHandler<T>;
void Unsubscribe<T, TH>() where T : IntegrationEvent where TH : IIntegrationEventHandler<T>;
void SubscribeDynamic<TH>(string eventName) where TH : IDynamicIntegrationEventHandler;
void UnsubscribeDynamic<TH>(string eventName) where TH : IDynamicIntegrationEventHandler;
}
}

View File

@ -0,0 +1,11 @@
using ZeroFramework.EventBus.Events;
namespace ZeroFramework.EventBus.Abstractions
{
public interface IIntegrationEventHandler { }
public interface IIntegrationEventHandler<in TIntegrationEvent> : IIntegrationEventHandler where TIntegrationEvent : IntegrationEvent
{
Task HandleAsync(TIntegrationEvent @event);
}
}

View File

@ -0,0 +1,21 @@
namespace ZeroFramework.EventBus.Events
{
public class IntegrationEvent
{
public IntegrationEvent()
{
Id = Guid.NewGuid();
CreationTime = DateTimeOffset.Now;
}
public IntegrationEvent(Guid id, DateTime createDate)
{
Id = id;
CreationTime = createDate;
}
public Guid Id { get; set; }
public DateTimeOffset CreationTime { get; set; }
}
}

View File

@ -0,0 +1,27 @@
namespace ZeroFramework.EventBus.Extensions
{
public static class GenericTypeExtensions
{
public static string GetGenericTypeName(this Type type)
{
var typeName = string.Empty;
if (type.IsGenericType)
{
var genericTypes = string.Join(",", type.GetGenericArguments().Select(t => t.Name).ToArray());
typeName = $"{type.Name.Remove(type.Name.IndexOf('`'))}<{genericTypes}>";
}
else
{
typeName = type.Name;
}
return typeName;
}
public static string GetGenericTypeName(this object @object)
{
return @object.GetType().GetGenericTypeName();
}
}
}

View File

@ -0,0 +1,35 @@
using ZeroFramework.EventBus.Abstractions;
using ZeroFramework.EventBus.Events;
using static ZeroFramework.EventBus.InMemoryEventBusSubscriptionsManager;
namespace ZeroFramework.EventBus
{
public interface IEventBusSubscriptionsManager
{
bool IsEmpty { get; }
event EventHandler<string> OnEventRemoved;
void AddDynamicSubscription<TH>(string eventName) where TH : IDynamicIntegrationEventHandler;
void RemoveDynamicSubscription<TH>(string eventName) where TH : IDynamicIntegrationEventHandler;
void AddSubscription<T, TH>() where T : IntegrationEvent where TH : IIntegrationEventHandler<T>;
void RemoveSubscription<T, TH>() where TH : IIntegrationEventHandler<T> where T : IntegrationEvent;
bool HasSubscriptionsForEvent<T>() where T : IntegrationEvent;
bool HasSubscriptionsForEvent(string eventName);
Type GetEventTypeByName(string eventName);
void Clear();
IEnumerable<SubscriptionInfo> GetHandlersForEvent<T>() where T : IntegrationEvent;
IEnumerable<SubscriptionInfo> GetHandlersForEvent(string eventName);
string GetEventKey<T>();
}
}

View File

@ -0,0 +1,147 @@
using ZeroFramework.EventBus.Abstractions;
using ZeroFramework.EventBus.Events;
namespace ZeroFramework.EventBus
{
public partial class InMemoryEventBusSubscriptionsManager : IEventBusSubscriptionsManager
{
private readonly Dictionary<string, List<SubscriptionInfo>> _handlers;
private readonly List<Type> _eventTypes;
public event EventHandler<string>? OnEventRemoved;
public InMemoryEventBusSubscriptionsManager()
{
_handlers = [];
_eventTypes = [];
}
public bool IsEmpty => !_handlers.Keys.Any();
public void Clear() => _handlers.Clear();
public void AddDynamicSubscription<TH>(string eventName) where TH : IDynamicIntegrationEventHandler
{
DoAddSubscription(typeof(TH), eventName, isDynamic: true);
}
public void AddSubscription<T, TH>() where T : IntegrationEvent where TH : IIntegrationEventHandler<T>
{
var eventName = GetEventKey<T>();
DoAddSubscription(typeof(TH), eventName, isDynamic: false);
if (!_eventTypes.Contains(typeof(T)))
{
_eventTypes.Add(typeof(T));
}
}
private void DoAddSubscription(Type handlerType, string eventName, bool isDynamic)
{
if (!HasSubscriptionsForEvent(eventName))
{
_handlers.Add(eventName, []);
}
if (_handlers[eventName].Any(s => s.HandlerType == handlerType))
{
throw new ArgumentException($"Handler Type {handlerType.Name} already registered for '{eventName}'", nameof(handlerType));
}
if (isDynamic)
{
_handlers[eventName].Add(SubscriptionInfo.Dynamic(handlerType));
}
else
{
_handlers[eventName].Add(SubscriptionInfo.Typed(handlerType));
}
}
public void RemoveDynamicSubscription<TH>(string eventName) where TH : IDynamicIntegrationEventHandler
{
var handlerToRemove = FindDynamicSubscriptionToRemove<TH>(eventName);
if (handlerToRemove is not null)
{
DoRemoveHandler(eventName, handlerToRemove);
}
}
public void RemoveSubscription<T, TH>() where TH : IIntegrationEventHandler<T> where T : IntegrationEvent
{
var handlerToRemove = FindSubscriptionToRemove<T, TH>();
var eventName = GetEventKey<T>();
if (handlerToRemove is not null)
{
DoRemoveHandler(eventName, handlerToRemove);
}
}
private void DoRemoveHandler(string eventName, SubscriptionInfo subsToRemove)
{
if (subsToRemove != null)
{
_handlers[eventName].Remove(subsToRemove);
if (!_handlers[eventName].Any())
{
_handlers.Remove(eventName);
var eventType = _eventTypes.SingleOrDefault(e => e.Name == eventName);
if (eventType != null)
{
_eventTypes.Remove(eventType);
}
RaiseOnEventRemoved(eventName);
}
}
}
public IEnumerable<SubscriptionInfo> GetHandlersForEvent<T>() where T : IntegrationEvent
{
var key = GetEventKey<T>();
return GetHandlersForEvent(key);
}
public IEnumerable<SubscriptionInfo> GetHandlersForEvent(string eventName) => _handlers[eventName];
private void RaiseOnEventRemoved(string eventName)
{
var handler = OnEventRemoved;
handler?.Invoke(this, eventName);
}
private SubscriptionInfo? FindDynamicSubscriptionToRemove<TH>(string eventName) where TH : IDynamicIntegrationEventHandler
{
return DoFindSubscriptionToRemove(eventName, typeof(TH));
}
private SubscriptionInfo? FindSubscriptionToRemove<T, TH>() where T : IntegrationEvent where TH : IIntegrationEventHandler<T>
{
var eventName = GetEventKey<T>();
return DoFindSubscriptionToRemove(eventName, typeof(TH));
}
private SubscriptionInfo? DoFindSubscriptionToRemove(string eventName, Type handlerType)
{
if (HasSubscriptionsForEvent(eventName))
{
return _handlers[eventName].Single(s => s.HandlerType == handlerType);
}
return null;
}
public bool HasSubscriptionsForEvent<T>() where T : IntegrationEvent
{
var key = GetEventKey<T>();
return HasSubscriptionsForEvent(key);
}
public bool HasSubscriptionsForEvent(string eventName) => _handlers.ContainsKey(eventName);
public Type GetEventTypeByName(string eventName) => _eventTypes.Single(t => t.Name == eventName);
public string GetEventKey<T>() => typeof(T).Name;
}
}

View File

@ -0,0 +1,28 @@
namespace ZeroFramework.EventBus
{
public partial class InMemoryEventBusSubscriptionsManager : IEventBusSubscriptionsManager
{
public class SubscriptionInfo
{
public bool IsDynamic { get; }
public Type HandlerType { get; }
private SubscriptionInfo(bool isDynamic, Type handlerType)
{
IsDynamic = isDynamic;
HandlerType = handlerType;
}
public static SubscriptionInfo Dynamic(Type handlerType)
{
return new SubscriptionInfo(true, handlerType);
}
public static SubscriptionInfo Typed(Type handlerType)
{
return new SubscriptionInfo(false, handlerType);
}
}
}
}

View File

@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

109
README.md Normal file
View File

@ -0,0 +1,109 @@

♥ 项目基本介绍
零度框架是一套基于微服务和领域模型驱动设计的企业级快速开发框架,基于微软 .NET 7+ 最新技术栈构建,容器化微服务最佳实践,零度框架的搭建以开发简单,多屏体验,前后端分离,灵活部署,最少依赖,最新框架为原则,以物联网平台管理系统为业务模型,参考诸多优秀开源框架,采用主流稳定的技术栈,从零开始搭建企业级架构。
后端技术Visual Studio 2022 + C# 12.0 + .NET 8.0 + ASP.NET Core + EF Core
前端技术Visual Studio Code + Node.js + TypeScript + React + ANTD
视频教程https://www.xcode.me/Training/Module/250
演示地址https://cloud.helloworldnet.com
演示账号:用户名 admin 密码 guest
[]
♥ 在 Visual Studio 2022 中运行后端微服务
1、首先使用 EF Code First 使用代码生成数据库,为了简化操作,以下脚本可完成数据库删除创建迁移并重建数据库,在 Visual Studio 选择「工具」->「NuGet包管理器」->「程序包管理控制台」中执行以下命令即可:
Drop-Database -Context DeviceCenterDbContext -Project ZeroFramework.DeviceCenter.Infrastructure -StartupProject ZeroFramework.DeviceCenter.Infrastructure -Confirm:$false
Drop-Database -Context ApplicationDbContext -Project ZeroFramework.IdentityServer.API -StartupProject ZeroFramework.IdentityServer.API -Confirm:$false
Remove-Migration -Context PersistedGrantDbContext -Project ZeroFramework.IdentityServer.API -StartupProject ZeroFramework.IdentityServer.API
Remove-Migration -Context ConfigurationDbContext -Project ZeroFramework.IdentityServer.API -StartupProject ZeroFramework.IdentityServer.API
Remove-Migration -Context ApplicationDbContext -Project ZeroFramework.IdentityServer.API -StartupProject ZeroFramework.IdentityServer.API
Remove-Migration -Context DeviceCenterDbContext -Project ZeroFramework.DeviceCenter.Infrastructure -StartupProject ZeroFramework.DeviceCenter.Infrastructure
Add-Migration InitialCreate -c PersistedGrantDbContext -o Migrations/PersistedGrantMigrations -Project ZeroFramework.IdentityServer.API -StartupProject ZeroFramework.IdentityServer.API
Add-Migration InitialCreate -c ConfigurationDbContext -o Migrations/ConfigurationMigrations -Project ZeroFramework.IdentityServer.API -StartupProject ZeroFramework.IdentityServer.API
Add-Migration InitialCreate -c ApplicationDbContext -o Migrations/ApplicationMigrations -Project ZeroFramework.IdentityServer.API -StartupProject ZeroFramework.IdentityServer.API
Add-Migration InitialCreate -Context DeviceCenterDbContext -Project ZeroFramework.DeviceCenter.Infrastructure -StartupProject ZeroFramework.DeviceCenter.Infrastructure
Update-Database -Context PersistedGrantDbContext -Project ZeroFramework.IdentityServer.API -StartupProject ZeroFramework.IdentityServer.API
Update-Database -Context ConfigurationDbContext -Project ZeroFramework.IdentityServer.API -StartupProject ZeroFramework.IdentityServer.API
Update-Database -Context ApplicationDbContext -Project ZeroFramework.IdentityServer.API -StartupProject ZeroFramework.IdentityServer.API
Update-Database -Context DeviceCenterDbContext -Project ZeroFramework.DeviceCenter.Infrastructure -StartupProject ZeroFramework.DeviceCenter.Infrastructure
2、设备数据通过「分桶模式」存储在 Mongdb 数据库中,因此你需要安装并启动 Mongdb 数据库,并在配置文件中修改连接字符串。
3、依次启动 ZeroFramework.IdentityServer.API 和 ZeroFramework.DeviceCenter.API 项目,容器、网关和聚合可暂时不配置,后期根据需要进行配置。
♥ 在 Visual Studio Code 中运行前端站点
1、首先确保系统已安装 Visual Studio Code 工具和 Node.js 环境,并进入 ZeroFramework.DeviceCenter.Web 目录。
2、在目录启动命令行并运行「npm install --global yarn」 和 「yarn install」 即可安装前端所需的 NPM 包,如果失败,可重试几次。
3、在目录启动命令行并运行「npm run dev」即可启动编译使用 「npm run build 」编译会生成 dist 目录,该目录可直接部署到生产平台。
♥ 项目更新记录
+ 所有项目框架及其用到的包已经升级到 .NET 7 最新版,并消除了很多警告和建议。
+ 在配置文件中添加了 「"UseDemoLaunchMode": true」以表示以演示模式运行演示模式使用 EF Core 拦截器禁用了编辑和删除操作。
♥ 项目目录结构说明
zeroframework「项目总目录」
├── ApiGateways「网关和聚合」
│ └── ZeroFramework.ReverseProxy「网关与反向代理」
│ ├── Program.cs
│ ├── Properties
│ ├── ZeroFramework.ReverseProxy.csproj
│ ├── appsettings.Development.json
│ ├── appsettings.json
│ ├── bin
│ └── obj
├── BuildingBlocks「公共中间件」
│ └── EventBus
│ ├── ZeroFramework.EventBus「事件总线抽象」
│ ├── ZeroFramework.EventBus.MemoryQueue「基于内存的队列」
│ └── ZeroFramework.EventBus.RabbitMQ「分布式队列」
├── Services「微服务」
│ ├── DeviceCenter「基于领域驱动的设备中心微服务」
│ │ ├── ZeroFramework.DeviceCenter.API「开放接口」
│ │ ├── ZeroFramework.DeviceCenter.Application「应用层」
│ │ ├── ZeroFramework.DeviceCenter.BackgroundTasks「生成演示」
│ │ ├── ZeroFramework.DeviceCenter.Domain「领域层」
│ │ └── ZeroFramework.DeviceCenter.Infrastructure「基础设施层」
│ └── Identity「认证和授权微服务」
│ └── ZeroFramework.IdentityServer.API「OAuth2.0开放接口」
├── Web
│ └── ZeroFramework.DeviceCenter.Web「基于 ANTD 的前端站点」
│ ├── README.md
│ ├── ZeroFramework.DeviceCenter.Web.esproj
│ ├── config
│ ├── jest.config.js
│ ├── jsconfig.json
│ ├── mock
│ ├── nuget.config
│ ├── package.json
│ ├── public
│ ├── src
│ ├── tests
│ └── tsconfig.json
└── ZeroFramework.sln「解决方案」
♥ 前端学习技术资料整理
TypeScripthttp://www.patrickzhong.com/TypeScript
Reacthttps://react.docschina.org/docs/getting-started.html
ECMAScripthttps://es6.ruanyifeng.com
ANTDhttps://ant.design/docs/react/introduce-cn
ANTD PROhttps://pro.ant.design/zh-CN/docs/overview

View File

@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "6.0.3",
"commands": [
"dotnet-ef"
]
}
}
}

View File

@ -0,0 +1,9 @@
namespace ZeroFramework.DeviceCenter.API.Constants
{
public class TenantClaimTypes
{
public const string TenantId = "tenant_id";
public const string TenantName = "tenant_name";
}
}

View File

@ -0,0 +1,181 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using System.Globalization;
using System.Reflection;
using System.Text.RegularExpressions;
using ZeroFramework.DeviceCenter.Application.Services.Permissions;
namespace ZeroFramework.DeviceCenter.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class ConfigurationsController(ILogger<ConfigurationsController> logger,
IHttpContextAccessor httpContextAccessor,
IOptions<AuthorizationOptions> authorizationOptions,
IOptions<RequestLocalizationOptions> requestLocalizationOptions,
IPermissionDefinitionManager permissionDefinitionManager,
IPermissionChecker permissionChecker,
IAuthorizationService authorizationService,
IStringLocalizerFactory stringLocalizerFactory) : ControllerBase
{
private readonly ILogger<ConfigurationsController> _logger = logger ?? NullLogger<ConfigurationsController>.Instance;
private readonly AuthorizationOptions _authorizationOptions = authorizationOptions.Value;
private readonly RequestLocalizationOptions _requestLocalizationOptions = requestLocalizationOptions.Value;
private readonly IPermissionDefinitionManager _permissionDefinitionManager = permissionDefinitionManager;
private readonly IPermissionChecker _permissionChecker = permissionChecker;
private readonly IAuthorizationService _authorizationService = authorizationService;
private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor;
private readonly IStringLocalizerFactory _stringLocalizerFactory = stringLocalizerFactory;
[HttpGet]
[AllowAnonymous]
public async Task<ApplicationConfiguration> GetAsync()
{
_logger.LogDebug("Executing ConfigurationApplicationService.GetAsync()...");
var result = new ApplicationConfiguration
{
Permissions = await GetPermissionConfigurationAsync()
};
_logger.LogDebug("Executed ConfigurationApplicationService.GetAsync().");
return result;
}
[Serializable]
public class ApplicationConfiguration
{
public PermissionConfiguration? Permissions { get; set; }
public LocalizationConfiguration? Localizations { get; set; }
}
[Serializable]
public class PermissionConfiguration
{
public Dictionary<string, bool> Policies { get; set; }
public Dictionary<string, bool> GrantedPolicies { get; set; }
public PermissionConfiguration()
{
Policies = [];
GrantedPolicies = [];
}
}
[Serializable]
public class LocalizationConfiguration
{
public IEnumerable<string>? SupportedCultures { get; set; }
public string CurrentCulture { get; set; } = CultureInfo.CurrentCulture.Name;
public Dictionary<string, Dictionary<string, string>>? Values { get; set; }
}
[NonAction]
private async Task<PermissionConfiguration> GetPermissionConfigurationAsync()
{
PermissionConfiguration permissionConfiguration = new();
IEnumerable<string> policyNames = _permissionDefinitionManager.GetPermissions().Select(p => p.Name);
PropertyInfo? policyMapProperty = typeof(AuthorizationOptions).GetProperty("PolicyMap", BindingFlags.Instance | BindingFlags.NonPublic);
if (policyMapProperty is not null)
{
object? policyMapPropertyValue = policyMapProperty.GetValue(_authorizationOptions);
if (policyMapPropertyValue is not null)
{
policyNames = policyNames.Union(((IDictionary<string, Task<AuthorizationPolicy>>)policyMapPropertyValue).Keys.ToList());
}
}
List<string> permissionPolicyNames = [];
List<string> otherPolicyNames = [];
foreach (var policyName in policyNames)
{
if (_permissionDefinitionManager.GetOrNull(policyName) is not null)
{
permissionPolicyNames.Add(policyName);
}
else
{
otherPolicyNames.Add(policyName);
}
}
foreach (var policyName in otherPolicyNames)
{
permissionConfiguration.Policies[policyName] = true;
if (_httpContextAccessor is not null && _httpContextAccessor.HttpContext is not null)
{
if ((await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, policyName)).Succeeded)
{
permissionConfiguration.GrantedPolicies[policyName] = true;
}
}
}
MultiplePermissionGrantResult result = await _permissionChecker.IsGrantedAsync(permissionPolicyNames.ToArray());
foreach (var (key, value) in result.Result)
{
permissionConfiguration.Policies[key] = true;
if (value == PermissionGrantResult.Granted)
{
permissionConfiguration.GrantedPolicies[key] = true;
}
}
return permissionConfiguration;
}
[NonAction]
#pragma warning disable IDE0051 // Remove unused private members
private async Task<LocalizationConfiguration> GetLocalizationConfigurationAsync()
#pragma warning restore IDE0051 // Remove unused private members
{
LocalizationConfiguration localizationConfiguration = new() { Values = [] };
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
string pattern = @"^(?<location>[a-zA-Z0-9.]+).Resources.(?<baseName>[a-zA-Z0-9.]+).([a-z]+-[A-Z]+.resources)$";
foreach (Assembly assembly in assemblies)
{
AssemblyName assemblyName = assembly.GetName();
if (assemblyName.Name is not null && assemblyName.Name.StartsWith("ZeroFramework"))
{
string[] resourceNames = assembly.GetManifestResourceNames();
foreach (var resourceName in resourceNames)
{
Match match = Regex.Match(resourceName, pattern, RegexOptions.IgnoreCase);
if (match.Success)
{
string baseName = match.Groups["baseName"].Value.TrimEnd('.');
string location = match.Groups["location"].Value;
var stringLocalizer = _stringLocalizerFactory.Create(baseName, location);
var dictionary = stringLocalizer.GetAllStrings(true).ToDictionary(s => s.Name, s => s.Value);
localizationConfiguration.Values.TryAdd($"{location}.{baseName}", dictionary);
}
}
}
}
localizationConfiguration.SupportedCultures = _requestLocalizationOptions.SupportedCultures?.Select(sc => sc.Name);
return await Task.FromResult(localizationConfiguration);
}
}
}

View File

@ -0,0 +1,76 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ZeroFramework.DeviceCenter.Application.Models.Devices;
using ZeroFramework.DeviceCenter.Application.PermissionProviders;
using ZeroFramework.DeviceCenter.Application.Services.Devices;
using ZeroFramework.DeviceCenter.Application.Services.Generics;
namespace ZeroFramework.DeviceCenter.API.Controllers
{
/// <summary>
/// For more information on enabling Web API for empty projects
/// </summary>
[Route("api/[controller]")]
[ApiController]
public class DeviceGroupsController(IDeviceGroupApplicationService deviceGroupService) : ControllerBase
{
private readonly IDeviceGroupApplicationService _deviceGroupService = deviceGroupService;
// GET: api/<DeviceGroupsController>
[HttpGet]
[Authorize(DeviceGroupPermissions.DeviceGroups.Default)]
public async Task<PagedResponseModel<DeviceGroupGetResponseModel>> GetDeviceGroups([FromQuery] DeviceGroupPagedRequestModel model)
{
return await _deviceGroupService.GetListAsync(model);
}
[HttpPut("Devices")]
[Authorize(DeviceGroupPermissions.DeviceGroups.Edit)]
public async Task<IActionResult> PutDevicesToGroup(int deviceGroupId, [FromBody] long[] deviceIds)
{
await _deviceGroupService.AddDevicesToGroup(deviceGroupId, deviceIds);
return Ok();
}
[HttpDelete("Devices")]
[Authorize(DeviceGroupPermissions.DeviceGroups.Edit)]
public async Task<IActionResult> DeleteDevicesFromGroup(int deviceGroupId, [FromBody] long[] deviceIds)
{
await _deviceGroupService.RemoveDevicesFromGroup(deviceGroupId, deviceIds);
return Ok();
}
// GET api/<DeviceGroupsController>/5
[HttpGet("{id}")]
[Authorize(DeviceGroupPermissions.DeviceGroups.Default)]
public async Task<DeviceGroupGetResponseModel> GetDeviceGroup(int id)
{
return await _deviceGroupService.GetAsync(id);
}
// POST api/<DeviceGroupsController>
[HttpPost]
[Authorize(DeviceGroupPermissions.DeviceGroups.Create)]
public async Task<DeviceGroupGetResponseModel> PostDeviceGroup([FromBody] DeviceGroupCreateRequestModel value)
{
return await _deviceGroupService.CreateAsync(value);
}
// PUT api/<DeviceGroupsController>/5
[HttpPut("{id}")]
[Authorize(DeviceGroupPermissions.DeviceGroups.Edit)]
public async Task<DeviceGroupGetResponseModel> PutDeviceGroup(int id, [FromBody] DeviceGroupUpdateRequestModel value)
{
value.Id = id;
return await _deviceGroupService.UpdateAsync(id, value);
}
// DELETE api/<DeviceGroupsController>/5
[HttpDelete("{id}")]
[Authorize(DeviceGroupPermissions.DeviceGroups.Delete)]
public async Task DeleteDeviceGroup(int id)
{
await _deviceGroupService.DeleteAsync(id);
}
}
}

View File

@ -0,0 +1,67 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ZeroFramework.DeviceCenter.Application.Models.Devices;
using ZeroFramework.DeviceCenter.Application.PermissionProviders;
using ZeroFramework.DeviceCenter.Application.Services.Devices;
using ZeroFramework.DeviceCenter.Application.Services.Generics;
namespace ZeroFramework.DeviceCenter.API.Controllers
{
/// <summary>
/// For more information on enabling Web API for empty projects
/// </summary>
[Route("api/[controller]")]
[ApiController]
public class DevicesController(IDeviceApplicationService deviceService) : ControllerBase
{
private readonly IDeviceApplicationService _deviceService = deviceService;
// GET: api/<DevicesController>
[HttpGet]
[Authorize(DevicePermissions.Devices.Default)]
public async Task<PagedResponseModel<DeviceGetResponseModel>> GetDevices([FromQuery] DevicePagedRequestModel model)
{
return await _deviceService.GetListAsync(model);
}
// GET api/<DevicesController>/5
[HttpGet("{id}")]
[Authorize(DevicePermissions.Devices.Default)]
public async Task<DeviceGetResponseModel> GetDevice(long id)
{
return await _deviceService.GetAsync(id);
}
// POST api/<DevicesController>
[HttpPost]
[Authorize(DevicePermissions.Devices.Create)]
public async Task<DeviceGetResponseModel> PostDevice([FromBody] DeviceCreateRequestModel value)
{
return await _deviceService.CreateAsync(value);
}
// PUT api/<DevicesController>/5
[HttpPut("{id}")]
[Authorize(DevicePermissions.Devices.Edit)]
public async Task<DeviceGetResponseModel> PutDevice(long id, [FromBody] DeviceUpdateRequestModel value)
{
value.Id = id;
return await _deviceService.UpdateAsync(id, value);
}
// DELETE api/<DevicesController>/5
[HttpDelete("{id}")]
[Authorize(DevicePermissions.Devices.Delete)]
public async Task DeleteDevice(long id)
{
await _deviceService.DeleteAsync(id);
}
[HttpGet("statistic")]
[Authorize(DevicePermissions.Devices.Default)]
public async Task<DeviceStatisticGetResponseModel> GetStatistic()
{
return await _deviceService.GetStatistics();
}
}
}

View File

@ -0,0 +1,60 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ZeroFramework.DeviceCenter.Application.Models.Products;
using ZeroFramework.DeviceCenter.Application.PermissionProviders;
using ZeroFramework.DeviceCenter.Application.Services.Generics;
using ZeroFramework.DeviceCenter.Application.Services.Products;
namespace ZeroFramework.DeviceCenter.API.Controllers
{
/// <summary>
/// For more information on enabling Web API for empty projects
/// </summary>
[Route("api/[controller]")]
[ApiController]
public class MeasurementUnitsController(IMeasurementUnitApplicationService productService) : ControllerBase
{
private readonly IMeasurementUnitApplicationService _productService = productService;
// GET: api/<MeasurementUnitsController>
[HttpGet]
[Authorize(ProductPermissions.MeasurementUnits.Default)]
public async Task<PagedResponseModel<MeasurementUnitGetResponseModel>> GetMeasurementUnits([FromQuery] MeasurementUnitPagedRequestModel model)
{
return await _productService.GetListAsync(model);
}
// GET api/<MeasurementUnitsController>/5
[HttpGet("{id}")]
[Authorize(ProductPermissions.MeasurementUnits.Default)]
public async Task<MeasurementUnitGetResponseModel> GetMeasurementUnit(int id)
{
return await _productService.GetAsync(id);
}
// POST api/<MeasurementUnitsController>
[HttpPost]
[Authorize(ProductPermissions.MeasurementUnits.Create)]
public async Task<MeasurementUnitGetResponseModel> PostMeasurementUnit([FromBody] MeasurementUnitCreateRequestModel value)
{
return await _productService.CreateAsync(value);
}
// PUT api/<MeasurementUnitsController>/5
[HttpPut("{id}")]
[Authorize(ProductPermissions.MeasurementUnits.Edit)]
public async Task<MeasurementUnitGetResponseModel> PutMeasurementUnit(int id, [FromBody] MeasurementUnitUpdateRequestModel value)
{
value.Id = id;
return await _productService.UpdateAsync(id, value);
}
// DELETE api/<MeasurementUnitsController>/5
[HttpDelete("{id}")]
[Authorize(ProductPermissions.MeasurementUnits.Delete)]
public async Task DeleteMeasurementUnit(int id)
{
await _productService.DeleteAsync(id);
}
}
}

View File

@ -0,0 +1,48 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ZeroFramework.DeviceCenter.Application.Models.Measurements;
using ZeroFramework.DeviceCenter.Application.PermissionProviders;
using ZeroFramework.DeviceCenter.Application.Services.Generics;
using ZeroFramework.DeviceCenter.Application.Services.Measurements;
namespace ZeroFramework.DeviceCenter.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class MeasurementsController(IDeviceDataApplicationService deviceDataApplication) : ControllerBase
{
private readonly IDeviceDataApplicationService _deviceDataApplication = deviceDataApplication;
[HttpGet("property-values")]
[Authorize(MeasurementPermissions.Measurements.DevicePropertyValues)]
public async Task<IEnumerable<DevicePropertyLastValue>?> GetDevicePropertyValues(int productId, long deviceId)
{
return await _deviceDataApplication.GetDevicePropertyValues(productId, deviceId);
}
[HttpGet("property-history-values")]
[Authorize(MeasurementPermissions.Measurements.DevicePropertyHistoryValues)]
public async Task<PageableListResposeModel<DevicePropertyValue>?> GetDevicePropertyHistoryValues(int productId, long deviceId, string identifier, DateTimeOffset startTime, DateTimeOffset endTime, SortingOrder sorting, int pageNumber, int pageSize)
{
int offset = (pageNumber - 1) * pageSize;
return await _deviceDataApplication.GetDevicePropertyHistoryValues(productId, deviceId, identifier, startTime, endTime, false, sorting, offset, pageSize);
}
[HttpGet("property-reports")]
[Authorize(MeasurementPermissions.Measurements.DevicePropertyReports)]
public async Task<PageableListResposeModel<DevicePropertyReport>?> GetDevicePropertyReports(int productId, long deviceId, string identifier, DateTimeOffset startTime, DateTimeOffset endTime, string reportType, int pageNumber, int pageSize)
{
int offset = (pageNumber - 1) * pageSize;
return await _deviceDataApplication.GetDevicePropertyReports(productId, deviceId, identifier, startTime, endTime, reportType, offset, pageSize);
}
[HttpPut("property-values")]
[Authorize(MeasurementPermissions.Measurements.SetDevicePropertyValues)]
public async Task SetDevicePropertyValue([FromQuery] int productId, [FromQuery] long deviceId, [FromBody] IDictionary<string, DevicePropertyValue> values)
{
await _deviceDataApplication.SetDevicePropertyValues(productId, deviceId, values);
}
}
}

View File

@ -0,0 +1,54 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ZeroFramework.DeviceCenter.Application.Models.Monitoring;
using ZeroFramework.DeviceCenter.Application.PermissionProviders;
using ZeroFramework.DeviceCenter.Application.Queries.Monitoring;
using ZeroFramework.DeviceCenter.Application.Services.Generics;
namespace ZeroFramework.DeviceCenter.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class MonitoringFactorsController(IMonitoringFactorQueries monitoringFactorQueries, ICrudApplicationService<int, MonitoringFactorGetResponseModel, MonitoringFactorPagedRequestModel, MonitoringFactorGetResponseModel, MonitoringFactorCreateRequestModel, MonitoringFactorUpdateRequestModel> crudApplicationService) : ControllerBase
{
private readonly ICrudApplicationService<int, MonitoringFactorGetResponseModel, MonitoringFactorPagedRequestModel, MonitoringFactorGetResponseModel, MonitoringFactorCreateRequestModel, MonitoringFactorUpdateRequestModel> _crudApplicationService = crudApplicationService;
private readonly IMonitoringFactorQueries _monitoringFactorQueries = monitoringFactorQueries;
[HttpGet]
[Authorize(MonitoringFactorPermissions.MonitoringFactors.Default)]
public async Task<PagedResponseModel<MonitoringFactorGetResponseModel>> GetMonitoringFactors([FromQuery] MonitoringFactorPagedRequestModel model)
{
return await _monitoringFactorQueries.GetMonitoringFactorsAsync(model);
}
[HttpGet("{id:int}")]
[Authorize(MonitoringFactorPermissions.MonitoringFactors.Default)]
public async Task<MonitoringFactorGetResponseModel> GetMonitoringFactor(int id)
{
return await _monitoringFactorQueries.GetMonitoringFactorAsync(id);
}
[HttpPost]
[Authorize(MonitoringFactorPermissions.MonitoringFactors.Create)]
public async Task<MonitoringFactorGetResponseModel> PostMonitoringFactor([FromBody] MonitoringFactorCreateRequestModel model)
{
return await _crudApplicationService.CreateAsync(model);
}
[HttpPut("{id:int}")]
[Authorize(MonitoringFactorPermissions.MonitoringFactors.Edit)]
public async Task<MonitoringFactorGetResponseModel> PutMonitoringFactor(int id, [FromBody] MonitoringFactorUpdateRequestModel model)
{
model.Id = id;
return await _crudApplicationService.UpdateAsync(id, model);
}
[HttpDelete("{id:int}")]
[Authorize(MonitoringFactorPermissions.MonitoringFactors.Delete)]
public async Task DeleteMonitoringFactor(int id)
{
await _crudApplicationService.DeleteAsync(id);
}
}
}

View File

@ -0,0 +1,42 @@
using MediatR;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
using System.Net;
using ZeroFramework.DeviceCenter.Application.Commands.Ordering;
using ZeroFramework.DeviceCenter.Application.Infrastructure;
using ZeroFramework.DeviceCenter.Application.Queries.Ordering;
namespace ZeroFramework.DeviceCenter.API.Controllers
{
[ApiExplorerSettings(IgnoreApi = true)]
public class OrdersController(IOrderQueries orderQueries, IMediator mediator) : Controller
{
private readonly IOrderQueries _orderQueries = orderQueries;
private readonly IMediator _mediator = mediator;
[HttpGet]
[ProducesResponseType(typeof(OrderViewModel), (int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<ActionResult> GetOrderAsync(Guid orderId)
{
//Todo: It's good idea to take advantage of GetOrderByIdQuery and handle by GetCustomerByIdQueryHandler
//var order customer = await _mediator.Send(new GetOrderByIdQuery(orderId));
OrderViewModel orderViewModel = await _orderQueries.GetOrderAsync(orderId);
return Ok(orderViewModel);
}
[HttpPut]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
public async Task<IActionResult> CancelOrderAsync([FromBody] CancelOrderCommand command, [FromHeader(Name = "X-Request-Id")] string? requestId)
{
requestId ??= Activity.Current?.Id ?? HttpContext.TraceIdentifier;
var identifiedCommand = new IdentifiedCommand<CancelOrderCommand, bool>(command, requestId);
await _mediator.Send(identifiedCommand);
return Ok();
}
}
}

View File

@ -0,0 +1,29 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ZeroFramework.DeviceCenter.Application.Models.Permissions;
using ZeroFramework.DeviceCenter.Application.PermissionProviders;
using ZeroFramework.DeviceCenter.Application.Services.Permissions;
namespace ZeroFramework.DeviceCenter.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class PermissionsController(IPermissionApplicationService permissionApplicationService) : ControllerBase
{
private readonly IPermissionApplicationService _permissionApplicationService = permissionApplicationService;
[HttpPut]
[Authorize(PermissionPermissions.Permissions.Edit)]
public virtual Task UpdateAsync(PermissionUpdateRequestModel updateModel)
{
return _permissionApplicationService.UpdateAsync(updateModel);
}
[HttpGet]
[Authorize(PermissionPermissions.Permissions.Get)]
public virtual Task<PermissionListResponseModel> GetAsync(string? providerName, string? providerKey, Guid? resourceGroupId)
{
return _permissionApplicationService.GetAsync(providerName, providerKey, resourceGroupId);
}
}
}

View File

@ -0,0 +1,74 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
using System.Net;
using ZeroFramework.DeviceCenter.Application.Commands.Products;
using ZeroFramework.DeviceCenter.Application.Infrastructure;
using ZeroFramework.DeviceCenter.Application.Models.Products;
using ZeroFramework.DeviceCenter.Application.PermissionProviders;
using ZeroFramework.DeviceCenter.Application.Services.Generics;
using ZeroFramework.DeviceCenter.Application.Services.Products;
namespace ZeroFramework.DeviceCenter.API.Controllers
{
/// <summary>
/// For more information on enabling Web API for empty projects
/// </summary>
[Route("api/[controller]")]
[ApiController]
public class ProductsController(IProductApplicationService productService, IMediator mediator) : ControllerBase
{
private readonly IProductApplicationService _productService = productService;
private readonly IMediator _mediator = mediator;
// GET: api/<ProductsController>
[HttpGet]
[Authorize(ProductPermissions.Products.Default)]
public async Task<PagedResponseModel<ProductGetResponseModel>> GetProducts([FromQuery] ProductPagedRequestModel model)
{
return await _productService.GetListAsync(model);
}
// GET api/<ProductsController>/5
[HttpGet("{id}")]
[Authorize(ProductPermissions.Products.Default)]
public async Task<ProductGetResponseModel> GetProduct(int id)
{
return await _productService.GetAsync(id);
}
// POST api/<ProductsController>
[HttpPost]
[Authorize(ProductPermissions.Products.Create)]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
public async Task<IActionResult> PostProduct([FromBody] CreateProductCommand command, [FromHeader(Name = "X-Request-Id")] string? requestId)
{
requestId ??= Activity.Current?.Id ?? HttpContext.TraceIdentifier;
var identifiedCommand = new IdentifiedCommand<CreateProductCommand, ProductGetResponseModel>(command, requestId);
ProductGetResponseModel result = await _mediator.Send(identifiedCommand);
return CreatedAtAction(nameof(GetProduct), new { id = result.Id }, result);
}
// PUT api/<ProductsController>/5
[HttpPut("{id}")]
[Authorize(ProductPermissions.Products.Edit)]
public async Task<ProductGetResponseModel> PutProduct(int id, [FromBody] ProductUpdateRequestModel value)
{
value.Id = id;
return await _productService.UpdateAsync(id, value);
}
// DELETE api/<ProductsController>/5
[HttpDelete("{id}")]
[Authorize(ProductPermissions.Products.Delete)]
public async Task DeleteProduct(int id)
{
await _productService.DeleteAsync(id);
}
}
}

View File

@ -0,0 +1,59 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ZeroFramework.DeviceCenter.Application.Models.Projects;
using ZeroFramework.DeviceCenter.Application.PermissionProviders;
using ZeroFramework.DeviceCenter.Application.Services.Generics;
namespace ZeroFramework.DeviceCenter.API.Controllers
{
/// <summary>
/// For more information on enabling Web API for empty projects
/// </summary>
[Route("api/[controller]")]
[ApiController]
public class ProjectsController(ICrudApplicationService<int, ProjectGetResponseModel, PagedRequestModel, ProjectGetResponseModel, ProjectCreateOrUpdateRequestModel, ProjectCreateOrUpdateRequestModel> crudService) : ControllerBase
{
private readonly ICrudApplicationService<int, ProjectGetResponseModel, PagedRequestModel, ProjectGetResponseModel, ProjectCreateOrUpdateRequestModel, ProjectCreateOrUpdateRequestModel> _crudService = crudService;
// GET: api/<ProjectsController>
[HttpGet]
[Authorize(ProjectPermissions.Projects.Default)]
public async Task<PagedResponseModel<ProjectGetResponseModel>> Get([FromQuery] PagedRequestModel model)
{
return await _crudService.GetListAsync(model);
}
// GET api/<ProjectsController>/5
[HttpGet("{id}")]
[Authorize(ProjectPermissions.Projects.Default)]
public async Task<ProjectGetResponseModel> Get(int id)
{
return await _crudService.GetAsync(id);
}
// POST api/<ProjectsController>
[HttpPost]
[Authorize(ProjectPermissions.Projects.Create)]
public async Task<ProjectGetResponseModel> Post([FromBody] ProjectCreateOrUpdateRequestModel value)
{
return await _crudService.CreateAsync(value);
}
// PUT api/<ProjectsController>/5
[HttpPut("{id}")]
[Authorize(ProjectPermissions.Projects.Edit)]
public async Task<ProjectGetResponseModel> Put(int id, [FromBody] ProjectCreateOrUpdateRequestModel value)
{
value.Id = id;
return await _crudService.UpdateAsync(id, value);
}
// DELETE api/<ProjectsController>/5
[HttpDelete("{id}")]
[Authorize(ProjectPermissions.Projects.Delete)]
public async Task Delete(int id)
{
await _crudService.DeleteAsync(id);
}
}
}

View File

@ -0,0 +1,52 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ZeroFramework.DeviceCenter.Application.Models.ResourceGroups;
using ZeroFramework.DeviceCenter.Application.PermissionProviders;
using ZeroFramework.DeviceCenter.Application.Services.Generics;
using ZeroFramework.DeviceCenter.Application.Services.ResourceGroups;
namespace ZeroFramework.DeviceCenter.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ResourceGroupsController(IResourceGroupApplicationService resourceGroupApplicationService) : ControllerBase
{
private readonly IResourceGroupApplicationService _resourceGroupApplicationService = resourceGroupApplicationService;
[HttpGet]
[Authorize(ResourceGroupPermissions.ResourceGroups.Default)]
public async Task<PagedResponseModel<ResourceGroupGetResponseModel>> GetResourceGroups([FromQuery] ResourceGroupPagedRequestModel model)
{
return await _resourceGroupApplicationService.GetListAsync(model);
}
[HttpGet("{id:guid}")]
[Authorize(ResourceGroupPermissions.ResourceGroups.Default)]
public async Task<ResourceGroupGetResponseModel> GetResourceGroup(Guid id)
{
return await _resourceGroupApplicationService.GetAsync(id);
}
[HttpPost]
[Authorize(ResourceGroupPermissions.ResourceGroups.Create)]
public async Task<ResourceGroupGetResponseModel> PostResourceGroup([FromBody] ResourceGroupCreateRequestModel model)
{
return await _resourceGroupApplicationService.CreateAsync(model);
}
[HttpPut("{id:guid}")]
[Authorize(ResourceGroupPermissions.ResourceGroups.Edit)]
public async Task<ResourceGroupGetResponseModel> PutResourceGroup(Guid id, [FromBody] ResourceGroupUpdateRequestModel model)
{
model.Id = id;
return await _resourceGroupApplicationService.UpdateAsync(id, model);
}
[HttpDelete("{id:guid}")]
[Authorize(ResourceGroupPermissions.ResourceGroups.Delete)]
public async Task DeleteResourceGroup(Guid id)
{
await _resourceGroupApplicationService.DeleteAsync(id);
}
}
}

View File

@ -0,0 +1,81 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ZeroFramework.DeviceCenter.Application.Models.Tenants;
using ZeroFramework.DeviceCenter.Application.PermissionProviders;
using ZeroFramework.DeviceCenter.Application.Services.Generics;
using ZeroFramework.DeviceCenter.Application.Services.Tenants;
namespace ZeroFramework.DeviceCenter.API.Controllers
{
/// <summary>
/// For more information on enabling Web API for empty projects
/// </summary>
[Route("api/[controller]")]
[ApiController, ApiExplorerSettings(IgnoreApi = true)]
public class TenantsController(ITenantApplicationService tenantService) : ControllerBase
{
private readonly ITenantApplicationService _tenantService = tenantService;
// GET: api/<TenantsController>
[HttpGet]
[Authorize(TenantPermissions.Tenants.Default)]
public async Task<PagedResponseModel<TenantGetResponseModel>> Get([FromQuery] PagedRequestModel model)
{
return await _tenantService.GetListAsync(model);
}
// GET api/<TenantsController>/5
[HttpGet("{id}")]
[Authorize(TenantPermissions.Tenants.Default)]
public async Task<TenantGetResponseModel> Get(Guid id)
{
return await _tenantService.GetAsync(id);
}
// POST api/<TenantsController>
[HttpPost]
[Authorize(TenantPermissions.Tenants.Create)]
public async Task<TenantGetResponseModel> Post([FromBody] TenantCreateOrUpdateRequestModel value)
{
return await _tenantService.CreateAsync(value);
}
// PUT api/<TenantsController>/5
[HttpPut("{id}")]
[Authorize(TenantPermissions.Tenants.Edit)]
public async Task<TenantGetResponseModel> Put(Guid id, [FromBody] TenantCreateOrUpdateRequestModel value)
{
value.Id = id;
return await _tenantService.UpdateAsync(id, value);
}
// DELETE api/<TenantsController>/5
[HttpDelete("{id}")]
[Authorize(TenantPermissions.Tenants.Delete)]
public async Task Delete(Guid id)
{
await _tenantService.DeleteAsync(id);
}
[HttpGet("{id}/default-connection-string")]
[Authorize(TenantPermissions.Tenants.ConnectionString)]
public async Task<string?> GetDefaultConnectionStringAsync(Guid id)
{
return await _tenantService.GetDefaultConnectionStringAsync(id);
}
[HttpPut("{id}/default-connection-string")]
[Authorize(TenantPermissions.Tenants.ConnectionString)]
public async Task UpdateDefaultConnectionStringAsync(Guid id, string defaultConnectionString)
{
await _tenantService.UpdateDefaultConnectionStringAsync(id, defaultConnectionString);
}
[HttpDelete("{id}/default-connection-string")]
[Authorize(TenantPermissions.Tenants.ConnectionString)]
public async Task DeleteDefaultConnectionStringAsync(Guid id)
{
await _tenantService.DeleteDefaultConnectionStringAsync(id);
}
}
}

View File

@ -0,0 +1,68 @@
using FluentValidation;
using FluentValidation.Results;
using Microsoft.AspNetCore.Mvc;
using ZeroFramework.DeviceCenter.Application.Models.Measurements;
using ZeroFramework.DeviceCenter.Application.Models.Projects;
using ZeroFramework.DeviceCenter.Application.Services.Measurements;
using ZeroFramework.DeviceCenter.Domain.Aggregates.MeasurementAggregate;
namespace ZeroFramework.DeviceCenter.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
[ApiExplorerSettings(IgnoreApi = true)]
public class TestsController(IValidator<ProjectCreateOrUpdateRequestModel> validator, IDeviceDataApplicationService deviceDataApplicationService) : ControllerBase
{
private readonly IValidator<ProjectCreateOrUpdateRequestModel> _validator = validator;
private readonly IDeviceDataApplicationService _deviceDataApplicationService = deviceDataApplicationService;
[HttpGet]
public async Task<ActionResult<Measurement>> Get()
{
int productId = int.Parse("b4b9996c-beb5-4695-ad91-072eac1a6f89");
long deviceId = 10000;
Random random = new();
DateTimeOffset currentDateTime = DateTimeOffset.Now;
for (int i = 1; i < 45; i++)
{
currentDateTime = currentDateTime.AddMinutes(random.Next(5, 20));
Measurement value = new(currentDateTime.LocalDateTime);
value.Fields.Add("Value", i);
await _deviceDataApplicationService.SetDevicePropertyValues(productId, deviceId, new Dictionary<string, DevicePropertyValue>
{
{
"Abc",
new DevicePropertyValue
{
Timestamp = currentDateTime.ToUnixTimeMilliseconds(),
Value = i
}
}
});
}
return Ok("OK");
}
[HttpPost]
public async Task<ActionResult<ProjectGetResponseModel>> Post([FromBody] ProjectCreateOrUpdateRequestModel value)
{
ValidationResult validationResult = _validator.Validate(value);
if (validationResult.IsValid)
{
return await Task.FromResult(new ProjectGetResponseModel());
}
return BadRequest(validationResult);
}
}
}

View File

@ -0,0 +1,164 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Distributed;
using System.Text;
using System.Text.Json;
namespace ZeroFramework.DeviceCenter.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class ValuesController(IDistributedCache distributedCache) : ControllerBase
{
private readonly IDistributedCache _distributedCache = distributedCache;
const string memberkey = "9VQrbUztIWJGu9IhPPeK";
const string appkey = "2302600008";
const string secret = "288Hs33a";
const string apiEndpoint = "http://219.151.131.31:19103";
static readonly HttpClient httpClient = new() { BaseAddress = new Uri(apiEndpoint) };
static readonly Dictionary<string, string> geocodingMap = new()
{
{ "50000002041320029010", "108.6280,30.6700" }, // 龙驹一级
{ "50000002041320029011", "108.6190,30.6400" }, // 龙驹二级
{ "50000002041320029008", "108.5476,30.7365" }, // 龙滩电站
{ "50000002041320029009", "108.4380,30.7713" }, // 高洞子电站
};
[HttpGet]
public async Task<IActionResult> GetAsync()
{
Dictionary<string, object> deviceListparmeters = new()
{
{ nameof(memberkey), memberkey },
};
string deviceListReadAsString = await SendApiRequest(HttpMethod.Post, "api/dict/device/select", deviceListparmeters);
JsonElement deviceListData = JsonDocument.Parse(deviceListReadAsString).RootElement;
if (deviceListData.GetProperty("code").GetString() == "0")
{
return Ok(deviceListData);
}
List<object> result = [];
foreach (var device in deviceListData.GetProperty("result").EnumerateArray())
{
string? deviceid = device.GetProperty("deviceid").GetString() ?? string.Empty;
string? devicename = device.GetProperty("devicename").GetString();
int online = device.GetProperty("online").GetInt32();
string? devicepwd = device.GetProperty("devicepwd").GetString();
string? address = device.GetProperty("address").GetString();
Dictionary<string, object> playUriParmeters = new()
{
{ nameof(memberkey), memberkey },
{ nameof(deviceid), deviceid },
{ "networktype", 1 },
};
string? playUriReadAsString = await SendApiRequest(HttpMethod.Post, "/api/dict/media/live", playUriParmeters);
JsonElement playUriData = JsonDocument.Parse(playUriReadAsString).RootElement;
string? playUri = null;
if (playUriData.GetProperty("code").GetString() == "1")
{
playUri = playUriData.GetProperty("result").EnumerateObject().First(e => e.Name == "m3u8uri").Value.GetString();
}
string? location = null;
if (geocodingMap.ContainsKey(deviceid))
{
location = geocodingMap[deviceid];
}
result.Add(new { DeivceId = deviceid, DeviceName = devicename, Address = address, PlayUri = playUri, Online = Convert.ToBoolean(online), Location = location });
}
return Ok(result);
}
static async Task<string> SendApiRequest(HttpMethod httpMethod, string apiName, Dictionary<string, object> parmeters)
{
string sign = CreateSign(httpMethod, parmeters, secret);
Dictionary<string, object> commonParmeters = new()
{
{ nameof(appkey), appkey },
{ nameof(sign), sign },
};
StringBuilder stringBuilder = new();
foreach (var item in commonParmeters)
{
stringBuilder.Append($"{item.Key}={item.Value}&");
}
string requestUri = $"{apiName}?{stringBuilder.ToString().TrimEnd('&')}";
if (httpMethod == HttpMethod.Get)
{
foreach (var item in parmeters)
{
stringBuilder.Append($"{item.Key}={item.Value}&");
}
requestUri = $"{apiName}?{stringBuilder.ToString().TrimEnd('&')}";
return await httpClient.GetStringAsync(requestUri);
}
var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
string json = JsonSerializer.Serialize(new { Parmdata = parmeters }, options);
StringContent content = new(json, Encoding.UTF8, "application/json");
var result = await httpClient.PostAsync(requestUri, content);
return await result.Content.ReadAsStringAsync();
}
static string CreateSign(HttpMethod httpMethod, Dictionary<string, object> parmeters, string secret)
{
string signString = $"{secret}&&";
SortedDictionary<string, object> sortedParmeters = new(parmeters);
if (httpMethod == HttpMethod.Get)
{
foreach (var item in sortedParmeters)
{
signString += $"{item.Key}={item.Value}&";
}
signString = signString.TrimEnd('&');
}
else
{
signString += JsonSerializer.Serialize(parmeters);
}
return ComputeHashMd5(signString);
}
public static string ComputeHashMd5(string text)
{
using var provider = System.Security.Cryptography.MD5.Create();
StringBuilder builder = new();
foreach (byte b in provider.ComputeHash(Encoding.UTF8.GetBytes(text)))
{
builder.Append(b.ToString("X2"));
}
return builder.ToString();
}
}
}

View File

@ -0,0 +1,30 @@
using Microsoft.AspNetCore.Authorization;
using ZeroFramework.DeviceCenter.API.Extensions.Authorization;
using ZeroFramework.DeviceCenter.API.Extensions.Hosting;
using ZeroFramework.DeviceCenter.Application.Services.Permissions;
namespace ZeroFramework.DeviceCenter.API
{
public static class DependencyRegistrar
{
public static IServiceCollection AddWebApiLayer(this IServiceCollection services)
{
var exportedTypes = System.Reflection.Assembly.GetExecutingAssembly().ExportedTypes;
var permissionDefinitionProviders = exportedTypes.Where(t => t.IsAssignableTo(typeof(IStartupFilter)));
permissionDefinitionProviders.ToList().ForEach(t => services.AddTransient(typeof(IStartupFilter), t));
services.AddLocalization(options => options.ResourcesPath = "Resources");
services.AddTransient<IPermissionChecker, PermissionChecker>();
services.AddSingleton<IAuthorizationPolicyProvider, CustomAuthorizationPolicyProvider>();
services.AddTransient<IAuthorizationHandler, PermissionRequirementHandler>();
services.AddTransient<IAuthorizationHandler, ResourcePermissionRequirementHandler>();
services.AddHttpContextAccessor();
services.AddHostedService<MockSampleWorker>();
return services;
}
}
}

View File

@ -0,0 +1,33 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.Extensions.Options;
using ZeroFramework.DeviceCenter.Application.Services.Permissions;
namespace ZeroFramework.DeviceCenter.API.Extensions.Authorization
{
public class CustomAuthorizationPolicyProvider(IOptions<AuthorizationOptions> options, IPermissionDefinitionManager permissionDefinitionManager) : DefaultAuthorizationPolicyProvider(options)
{
private readonly IPermissionDefinitionManager _permissionDefinitionManager = permissionDefinitionManager;
public async override Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
{
AuthorizationPolicy? policy = await base.GetPolicyAsync(policyName);
if (policy is not null)
{
return policy;
}
var permission = _permissionDefinitionManager.GetOrNull(policyName);
if (permission is not null)
{
var policyBuilder = new AuthorizationPolicyBuilder(Array.Empty<string>());
policyBuilder.Requirements.Add(new OperationAuthorizationRequirement { Name = policyName });
return policyBuilder.Build();
}
return null;
}
}
}

View File

@ -0,0 +1,188 @@
using System.Security.Claims;
using ZeroFramework.DeviceCenter.Application.Services.Permissions;
using ZeroFramework.DeviceCenter.Domain.Aggregates.ResourceGroupAggregate;
using ZeroFramework.DeviceCenter.Domain.Repositories;
namespace ZeroFramework.DeviceCenter.API.Extensions.Authorization
{
public class PermissionChecker(IHttpContextAccessor httpContextAccessor, IPermissionDefinitionManager permissionDefinitionManager, IEnumerable<IPermissionValueProvider> permissionValueProviders, IRepository<ResourceGrouping, Guid> resourceGroupingRepository) : IPermissionChecker
{
private readonly IPermissionDefinitionManager _permissionDefinitionManager = permissionDefinitionManager;
private readonly IEnumerable<IPermissionValueProvider> _permissionValueProviders = permissionValueProviders;
private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor;
private readonly IRepository<ResourceGrouping, Guid> _resourceGroupingRepository = resourceGroupingRepository;
public async Task<bool> IsGrantedAsync(string name, Guid? resourceGroupId) => await IsGrantedAsync(_httpContextAccessor.HttpContext?.User ?? new ClaimsPrincipal(), name, resourceGroupId);
public async Task<bool> IsGrantedAsync(ClaimsPrincipal claimsPrincipal, string name, Guid? resourceGroupId)
{
if (name is null)
{
throw new ArgumentNullException(nameof(name));
}
PermissionDefinition permissionDefinition = _permissionDefinitionManager.Get(name);
if (!permissionDefinition.IsEnabled)
{
return false;
}
var isGranted = false;
foreach (var permissionValueProvider in _permissionValueProviders)
{
if (permissionDefinition.AllowedProviders.Any() && !permissionDefinition.AllowedProviders.Contains(permissionValueProvider.Name))
{
continue;
}
var result = await permissionValueProvider.CheckAsync(claimsPrincipal, permissionDefinition, resourceGroupId);
if (result == PermissionGrantResult.Granted)
{
isGranted = true;
}
else if (result == PermissionGrantResult.Prohibited)
{
return false;
}
}
return isGranted;
}
public async Task<MultiplePermissionGrantResult> IsGrantedAsync(string[] names, Guid? resourceGroupId) => await IsGrantedAsync(_httpContextAccessor.HttpContext?.User ?? new ClaimsPrincipal(), names, resourceGroupId);
public async Task<MultiplePermissionGrantResult> IsGrantedAsync(ClaimsPrincipal claimsPrincipal, string[] names, Guid? resourceGroupId)
{
MultiplePermissionGrantResult result = new();
names ??= Array.Empty<string>();
List<PermissionDefinition> permissionDefinitions = [];
foreach (string name in names)
{
var permission = _permissionDefinitionManager.Get(name);
result.Result.Add(name, PermissionGrantResult.Undefined);
if (permission.IsEnabled)
{
permissionDefinitions.Add(permission);
}
}
foreach (IPermissionValueProvider permissionValueProvider in _permissionValueProviders)
{
var pf = permissionDefinitions.Where(x => !x.AllowedProviders.Any() || x.AllowedProviders.Contains(permissionValueProvider.Name)).ToList();
var multipleResult = await permissionValueProvider.CheckAsync(claimsPrincipal, pf, resourceGroupId);
foreach (var grantResult in multipleResult.Result.Where(grantResult => result.Result.ContainsKey(grantResult.Key) && result.Result[grantResult.Key] == PermissionGrantResult.Undefined && grantResult.Value != PermissionGrantResult.Undefined))
{
result.Result[grantResult.Key] = grantResult.Value;
permissionDefinitions.RemoveAll(x => x.Name == grantResult.Key);
}
if (result.AllGranted || result.AllProhibited)
{
break;
}
}
return result;
}
public async Task<bool> IsGrantedAsync(string name, ResourceDescriptor resource) => await IsGrantedAsync(_httpContextAccessor.HttpContext?.User ?? new ClaimsPrincipal(), name, resource);
public async Task<bool> IsGrantedAsync(ClaimsPrincipal claimsPrincipal, string name, ResourceDescriptor resource)
{
var resourceGrouping = await _resourceGroupingRepository.FindAsync(e => e.Resource.ResourceId == resource);
return await IsGrantedAsync(claimsPrincipal, name, resourceGrouping?.ResourceGroupId);
}
public async Task<bool> IsGrantedAsync(string name) => await IsGrantedAsync(_httpContextAccessor.HttpContext?.User ?? new ClaimsPrincipal(), name);
public async Task<bool> IsGrantedAsync(ClaimsPrincipal claimsPrincipal, string name)
{
if (name is null)
{
throw new ArgumentNullException(nameof(name));
}
PermissionDefinition permissionDefinition = _permissionDefinitionManager.Get(name);
if (!permissionDefinition.IsEnabled)
{
return false;
}
var isGranted = false;
foreach (var permissionValueProvider in _permissionValueProviders)
{
if (permissionDefinition.AllowedProviders.Any() && !permissionDefinition.AllowedProviders.Contains(permissionValueProvider.Name))
{
continue;
}
var result = await permissionValueProvider.CheckAsync(claimsPrincipal, permissionDefinition, Guid.Empty);
if (result == PermissionGrantResult.Granted)
{
isGranted = true;
}
else if (result == PermissionGrantResult.Prohibited)
{
return false;
}
}
return isGranted;
}
public async Task<MultiplePermissionGrantResult> IsGrantedAsync(string[] names) => await IsGrantedAsync(_httpContextAccessor.HttpContext?.User ?? new ClaimsPrincipal(), names);
public async Task<MultiplePermissionGrantResult> IsGrantedAsync(ClaimsPrincipal claimsPrincipal, string[] names)
{
MultiplePermissionGrantResult result = new();
names ??= Array.Empty<string>();
List<PermissionDefinition> permissionDefinitions = [];
foreach (string name in names)
{
var permission = _permissionDefinitionManager.Get(name);
result.Result.Add(name, PermissionGrantResult.Undefined);
if (permission.IsEnabled)
{
permissionDefinitions.Add(permission);
}
}
foreach (IPermissionValueProvider permissionValueProvider in _permissionValueProviders)
{
var pfs = permissionDefinitions.Where(x => !x.AllowedProviders.Any() || x.AllowedProviders.Contains(permissionValueProvider.Name)).ToList();
var multipleResult = await permissionValueProvider.CheckAsync(claimsPrincipal, pfs, Guid.Empty);
foreach (var grantResult in multipleResult.Result.Where(grantResult => result.Result.ContainsKey(grantResult.Key) && result.Result[grantResult.Key] == PermissionGrantResult.Undefined && grantResult.Value != PermissionGrantResult.Undefined))
{
result.Result[grantResult.Key] = grantResult.Value;
permissionDefinitions.RemoveAll(x => x.Name == grantResult.Key);
}
if (result.AllGranted || result.AllProhibited)
{
break;
}
}
return result;
}
}
}

View File

@ -0,0 +1,33 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using ZeroFramework.DeviceCenter.Application.Services.Permissions;
using ZeroFramework.DeviceCenter.Domain.Aggregates.ResourceGroupAggregate;
namespace ZeroFramework.DeviceCenter.API.Extensions.Authorization
{
public class PermissionRequirementHandler(IPermissionChecker permissionChecker) : AuthorizationHandler<OperationAuthorizationRequirement>
{
private readonly IPermissionChecker _permissionChecker = permissionChecker;
protected async override Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement)
{
if (await _permissionChecker.IsGrantedAsync(context.User, requirement.Name))
{
context.Succeed(requirement);
}
}
}
public class ResourcePermissionRequirementHandler(IPermissionChecker permissionChecker) : AuthorizationHandler<OperationAuthorizationRequirement, ResourceDescriptor>
{
private readonly IPermissionChecker _permissionChecker = permissionChecker;
protected async override Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement, ResourceDescriptor resource)
{
if (await _permissionChecker.IsGrantedAsync(context.User, requirement.Name, resource))
{
context.Succeed(requirement);
}
}
}
}

View File

@ -0,0 +1,68 @@
using FluentValidation.AspNetCore;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using ZeroFramework.DeviceCenter.Application;
using ZeroFramework.DeviceCenter.Domain;
using ZeroFramework.DeviceCenter.Infrastructure;
[assembly: HostingStartup(typeof(ZeroFramework.DeviceCenter.API.Extensions.Hosting.CustomHostingStartup))]
namespace ZeroFramework.DeviceCenter.API.Extensions.Hosting
{
public class CustomHostingStartup : IHostingStartup
{
public void Configure(IWebHostBuilder builder)
{
builder.ConfigureServices((context, services) =>
{
services.AddFluentValidationAutoValidation();
services.AddDomainLayer();
services.AddInfrastructureLayer(context.Configuration);
services.AddApplicationLayer(context.Configuration);
services.AddWebApiLayer();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.TryAdd(nameof(ClaimTypes.Name).ToLower(), ClaimTypes.Name);
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority = context.Configuration.GetValue<string>("IdentityServer:AuthorizationUrl");
options.RequireHttpsMetadata = false;
options.TokenValidationParameters.ValidateAudience = false;
options.TokenValidationParameters.NameClaimType = ClaimTypes.Name;
});
services.AddCors(options =>
{
string[]? allowedOrigins = context.Configuration.GetSection("AllowedOrigins").Get<string[]>();
if (allowedOrigins is not null)
{
options.AddDefaultPolicy(builder => builder.WithOrigins(allowedOrigins).AllowAnyMethod().AllowAnyHeader().AllowCredentials());
}
});
string[] supportedCultures = new[] { "zh-CN", "en-US" };
services.AddRequestLocalization(options =>
{
options.ApplyCurrentCultureToResponseHeaders = false;
options.SetDefaultCulture(supportedCultures.First());
options.AddSupportedCultures(supportedCultures);
options.AddSupportedUICultures(supportedCultures);
});
services.Configure<Infrastructure.ConnectionStrings.TenantStoreOptions>(context.Configuration);
services.Configure<Microsoft.AspNetCore.Mvc.MvcOptions>(options =>
{
options.ModelBinderProviders.Add(new ModelBinding.SortingBinderProvider());
options.Filters.Add<HttpResponseExceptionFilter>();
});
});
}
}
}

View File

@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Mvc.Razor;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace ZeroFramework.DeviceCenter.API.Extensions.Hosting
{
public static class CustomMvcBuilderExtensions
{
public static IMvcBuilder AddCustomExtensions(this IMvcBuilder builder)
{
builder.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix).AddDataAnnotationsLocalization(options => options.DataAnnotationLocalizerProvider = (type, factory) => factory.Create(type));
builder.AddJsonOptions(configure =>
{
configure.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
});
return builder;
}
}
}

View File

@ -0,0 +1,79 @@
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Localization;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using ZeroFramework.DeviceCenter.Application.IntegrationEvents.EventHandling.Ordering;
using ZeroFramework.DeviceCenter.Application.IntegrationEvents.Events.Ordering;
using ZeroFramework.DeviceCenter.Domain.Repositories;
using ZeroFramework.EventBus.Abstractions;
namespace ZeroFramework.DeviceCenter.API.Extensions.Hosting
{
public class CustomStartupFilter : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return (app) =>
{
if (app.ApplicationServices.GetRequiredService<IWebHostEnvironment>().IsStaging())
{
var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>();
eventBus.SubscribeDynamic<OrderPaymentSucceededDynamicIntegrationEventHandler>(nameof(OrderPaymentSucceededIntegrationEvent));
eventBus.Subscribe<OrderPaymentFailedIntegrationEvent, OrderPaymentFailedIntegrationEventHandler>();
}
using (IServiceScope serviceScope = app.ApplicationServices.CreateScope())
{
var dataSeedProviders = serviceScope.ServiceProvider.GetServices<IDataSeedProvider>();
foreach (IDataSeedProvider dataSeedProvider in dataSeedProviders)
{
dataSeedProvider.SeedAsync(serviceScope.ServiceProvider).Wait();
}
};
IStringLocalizerFactory? localizerFactory = app.ApplicationServices.GetService<IStringLocalizerFactory>();
FluentValidation.ValidatorOptions.Global.DisplayNameResolver = (type, memberInfo, lambdaExpression) =>
{
string? displayName = string.Empty;
DisplayAttribute? displayColumnAttribute = memberInfo.GetCustomAttributes(true).OfType<DisplayAttribute>().FirstOrDefault();
if (displayColumnAttribute is not null)
{
displayName = displayColumnAttribute.Name;
}
DisplayNameAttribute? displayNameAttribute = memberInfo.GetCustomAttributes(true).OfType<DisplayNameAttribute>().FirstOrDefault();
if (displayNameAttribute is not null)
{
displayName = displayNameAttribute.DisplayName;
}
if (!string.IsNullOrWhiteSpace(displayName) && localizerFactory is not null)
{
return localizerFactory.Create(type)[displayName];
}
if (!string.IsNullOrWhiteSpace(displayName))
{
return displayName;
}
return memberInfo.Name;
};
app.UseCors();
app.UseRequestLocalization();
//Configure ASP.NET Core to work with proxy servers and load balancers
app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All });
next(app);
};
}
}
}

View File

@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace ZeroFramework.DeviceCenter.API.Extensions.Hosting
{
public class HttpResponseExceptionFilter : IActionFilter, IOrderedFilter
{
public int Order => int.MaxValue - 10;
public void OnActionExecuting(ActionExecutingContext context) { }
public void OnActionExecuted(ActionExecutedContext context)
{
if (context.Exception is InvalidOperationException ex && ex.Message == "DisalbeModifiedDeleted")
{
context.Result = new ObjectResult(new ProblemDetails
{
Status = 400,
Title = ex.Message,
Detail = "The demo system does not support edit and delete operations."
});
context.ExceptionHandled = true;
}
}
}
}

View File

@ -0,0 +1,104 @@
using ZeroFramework.DeviceCenter.Application.Models.Measurements;
using ZeroFramework.DeviceCenter.Application.Services.Measurements;
using ZeroFramework.DeviceCenter.Domain.Aggregates.DeviceAggregate;
using ZeroFramework.DeviceCenter.Domain.Aggregates.ProductAggregate;
using ZeroFramework.DeviceCenter.Domain.Repositories;
namespace ZeroFramework.DeviceCenter.API.Extensions.Hosting
{
public class MockSampleWorker(IRepository<Product, int> productRepository, IRepository<Device, long> deviceRepository, IDeviceDataApplicationService deviceDataApplicationService, ILogger<MockSampleWorker> logger) : BackgroundService
{
private readonly IRepository<Product, int> _productRepository = productRepository;
private readonly IRepository<Device, long> _deviceRepository = deviceRepository;
private readonly IDeviceDataApplicationService _deviceApplicationService = deviceDataApplicationService;
private readonly ILogger<MockSampleWorker> _logger = logger;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
try
{
await GenerateDeviceMockDataAsync(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Generate device mock data error");
}
await Task.Delay(TimeSpan.FromMinutes(8), stoppingToken);
}
}
private async Task GenerateDeviceMockDataAsync(CancellationToken stoppingToken)
{
List<Product> productList = await _productRepository.GetListAsync(cancellationToken: stoppingToken);
List<Device> deviceList = await _deviceRepository.GetListAsync(cancellationToken: stoppingToken);
Random random = new(Guid.NewGuid().GetHashCode());
_logger.LogInformation("Start generating mock data...");
foreach (Device device in deviceList)
{
var properties = productList.SingleOrDefault(e => e.Id == device.ProductId)?.Features?.Properties ?? Enumerable.Empty<PropertyFeature>();
foreach (var propery in properties)
{
DateTimeOffset startDate = DateTimeOffset.Now;
DevicePropertyValue devicePropertyValue = new()
{
Timestamp = startDate.ToUnixTimeMilliseconds()
};
if (propery.DataType.Type is DataTypeDefinitions.Int32 or DataTypeDefinitions.Int64)
{
devicePropertyValue.Value = random.Next(ushort.MaxValue);
}
if (propery.DataType.Type is DataTypeDefinitions.Float or DataTypeDefinitions.Double)
{
if (propery.DataType.Specs is not null)
{
int min = Convert.ToInt32(propery.DataType.Specs.First(e => e.Key == "minValue").Value?.ToString());
int max = Convert.ToInt32(propery.DataType.Specs.First(e => e.Key == "maxValue").Value?.ToString());
devicePropertyValue.Value = random.Next(min, max) + Random.Shared.NextDouble() * 10;
}
}
if (propery.DataType.Type is DataTypeDefinitions.Bool)
{
devicePropertyValue.Value = random.Next(0, 10) % 2 == 0;
}
if (propery.DataType.Type is DataTypeDefinitions.Date)
{
devicePropertyValue.Value = DateTimeOffset.Now.AddMilliseconds(-random.Next(int.MaxValue)).ToUnixTimeMilliseconds();
}
if (propery.DataType.Type is DataTypeDefinitions.String)
{
devicePropertyValue.Value = Path.GetRandomFileName();
}
if (devicePropertyValue.Value is not null)
{
await _deviceApplicationService.SetDevicePropertyValues(device.ProductId, device.Id, new Dictionary<string, DevicePropertyValue>
{
{propery.Identifier, devicePropertyValue }
});
}
}
}
_logger.LogInformation("All mock data generated.");
}
}
}

View File

@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using ZeroFramework.DeviceCenter.Application.Services.Generics;
namespace ZeroFramework.DeviceCenter.API.Extensions.ModelBinding
{
public class SortingBinderProvider : IModelBinderProvider
{
public IModelBinder? GetBinder(ModelBinderProviderContext context)
{
context = context ?? throw new ArgumentNullException(nameof(context));
if (context.Metadata.ModelType == typeof(IEnumerable<SortingDescriptor>))
{
return new SortingModelBinder();
}
return null;
}
}
}

View File

@ -0,0 +1,59 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System.Text.Json;
using ZeroFramework.DeviceCenter.Application.Services.Generics;
namespace ZeroFramework.DeviceCenter.API.Extensions.ModelBinding
{
public class SortingModelBinder : IModelBinder
{
/// <summary>
/// Custom Model Binding in ASP.NET Core
/// </summary>
public Task BindModelAsync(ModelBindingContext bindingContext)
{
bindingContext = bindingContext ?? throw new ArgumentNullException(nameof(bindingContext));
string modelName = bindingContext.ModelName;
// Try to fetch the value of the argument by name
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
if (valueProviderResult == ValueProviderResult.None)
{
return Task.CompletedTask;
}
bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);
string? value = valueProviderResult.FirstValue;
// Check if the argument value is null or empty
if (string.IsNullOrEmpty(value))
{
return Task.CompletedTask;
}
var sorter = JsonSerializer.Deserialize<IDictionary<string, string>>(value);
var sortingDirectionMap = new Dictionary<string, SortingOrder>
{
{ "ascend", SortingOrder.Ascending },
{ "descend", SortingOrder.Descending }
};
if (sorter is not null)
{
var effectSorter = sorter.Where(item => sortingDirectionMap.ContainsKey(item.Value));
var sorting = effectSorter.Select(item => new SortingDescriptor
{
PropertyName = item.Key,
SortDirection = sortingDirectionMap[item.Value]
});
bindingContext.Result = ModelBindingResult.Success(sorting);
}
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,51 @@
using ZeroFramework.DeviceCenter.API.Constants;
using ZeroFramework.DeviceCenter.Domain.Aggregates.TenantAggregate;
namespace ZeroFramework.DeviceCenter.API.Extensions.Tenants
{
public class TenantMiddleware(ICurrentTenant currentTenant) : IMiddleware
{
private readonly ICurrentTenant _currentTenant = currentTenant;
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
string? tenantIdString = ResolveTenantId(context);
if (Guid.TryParse(tenantIdString, out var parsedTenantId))
{
using (_currentTenant.Change(parsedTenantId))
{
await next(context);
}
}
else
{
await next(context);
}
}
protected virtual string? ResolveTenantId(HttpContext httpContext)
{
if (httpContext.Request.Headers.TryGetValue(TenantClaimTypes.TenantId, out var headerValues))
{
return headerValues.First();
}
if (httpContext.Request.Query.TryGetValue(TenantClaimTypes.TenantId, out var queryValues))
{
return queryValues.First();
}
if (httpContext.Request.Cookies.TryGetValue(TenantClaimTypes.TenantId, out var cookieValue))
{
return cookieValue;
}
if (httpContext.Request.RouteValues.TryGetValue(TenantClaimTypes.TenantId, out var routeValue))
{
return routeValue?.ToString();
}
return httpContext.User.FindFirst(TenantClaimTypes.TenantId)?.Value;
}
}
}

View File

@ -0,0 +1,15 @@
namespace ZeroFramework.DeviceCenter.API.Extensions.Tenants
{
public static class TenantMiddlewareExtensions
{
public static IServiceCollection AddTenantMiddleware(this IServiceCollection services)
{
return services.AddTransient<TenantMiddleware>();
}
public static IApplicationBuilder UseTenantMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<TenantMiddleware>();
}
}
}

View File

@ -0,0 +1,35 @@
using NLog;
using NLog.Web;
var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
try
{
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseNLog();
// Add services to the container.
var startup = new ZeroFramework.DeviceCenter.API.Startup(builder.Configuration);
startup.ConfigureServices(builder.Services);
var app = builder.Build();
// Configure the HTTP request pipeline.
startup.Configure(app, app.Environment);
app.Run();
}
catch (Exception exception)
{
// NLog: catch setup errors
logger.Error(exception, "Stopped program because of exception");
throw;
}
finally
{
// Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux)
LogManager.Shutdown();
}

View File

@ -0,0 +1,15 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"ZeroFramework.DeviceCenter.API": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:6001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,36 @@

## RabbitMQ For Docker
docker pull rabbitmq:management
docker run -d -p 5672:5672 -p 15672:15672 --name rabbitmq rabbitmq:management
## Microservices Endpoints
IdentityServer : https://localhost:5001
DeviceCenterAPI : https://localhost:6001
## Develop ASP.NET Core apps using a file watcher
dotnet watch --project .\Services\DeviceCenter\ZeroFramework.DeviceCenter.API run
dotnet watch --project .\Services\Identity\ZeroFramework.IdentityServer.API run
## Create DbContext Migrations
Drop-Database -Context DeviceCenterDbContext -Project ZeroFramework.DeviceCenter.Infrastructure -StartupProject ZeroFramework.DeviceCenter.Infrastructure -Confirm:$false
Drop-Database -Context ApplicationDbContext -Project ZeroFramework.IdentityServer.API -StartupProject ZeroFramework.IdentityServer.API -Confirm:$false
Remove-Migration -Context PersistedGrantDbContext -Project ZeroFramework.IdentityServer.API -StartupProject ZeroFramework.IdentityServer.API
Remove-Migration -Context ConfigurationDbContext -Project ZeroFramework.IdentityServer.API -StartupProject ZeroFramework.IdentityServer.API
Remove-Migration -Context ApplicationDbContext -Project ZeroFramework.IdentityServer.API -StartupProject ZeroFramework.IdentityServer.API
Remove-Migration -Context DeviceCenterDbContext -Project ZeroFramework.DeviceCenter.Infrastructure -StartupProject ZeroFramework.DeviceCenter.Infrastructure
Add-Migration InitialCreate -c PersistedGrantDbContext -o Migrations/PersistedGrantMigrations -Project ZeroFramework.IdentityServer.API -StartupProject ZeroFramework.IdentityServer.API
Add-Migration InitialCreate -c ConfigurationDbContext -o Migrations/ConfigurationMigrations -Project ZeroFramework.IdentityServer.API -StartupProject ZeroFramework.IdentityServer.API
Add-Migration InitialCreate -c ApplicationDbContext -o Migrations/ApplicationMigrations -Project ZeroFramework.IdentityServer.API -StartupProject ZeroFramework.IdentityServer.API
Add-Migration InitialCreate -Context DeviceCenterDbContext -Project ZeroFramework.DeviceCenter.Infrastructure -StartupProject ZeroFramework.DeviceCenter.Infrastructure
Update-Database -Context PersistedGrantDbContext -Project ZeroFramework.IdentityServer.API -StartupProject ZeroFramework.IdentityServer.API
Update-Database -Context ConfigurationDbContext -Project ZeroFramework.IdentityServer.API -StartupProject ZeroFramework.IdentityServer.API
Update-Database -Context ApplicationDbContext -Project ZeroFramework.IdentityServer.API -StartupProject ZeroFramework.IdentityServer.API
Update-Database -Context DeviceCenterDbContext -Project ZeroFramework.DeviceCenter.Infrastructure -StartupProject ZeroFramework.DeviceCenter.Infrastructure

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="HelloWorld" xml:space="preserve">
<value>Hello World!</value>
</data>
</root>

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="HelloWorld" xml:space="preserve">
<value>世界您好!</value>
</data>
</root>

View File

@ -0,0 +1,96 @@
using Microsoft.OpenApi.Models;
using ZeroFramework.API.Infrastructure.Swagger;
using ZeroFramework.DeviceCenter.API.Extensions.Hosting;
using ZeroFramework.DeviceCenter.API.Extensions.Tenants;
using ZeroFramework.DeviceCenter.Application.Services.Generics;
namespace ZeroFramework.DeviceCenter.API
{
public class Startup(IConfiguration configuration)
{
public IConfiguration Configuration { get; } = configuration;
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers().AddCustomExtensions();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Device Center API", Version = "v1" });
c.SupportNonNullableReferenceTypes();
c.UseAllOfToExtendReferenceSchemas();
c.CustomOperationIds(api =>
{
string? actionName = api.ActionDescriptor.RouteValues["action"];
if (actionName is not null)
{
return $"{System.Text.Json.JsonNamingPolicy.CamelCase.ConvertName(actionName)}";
}
return api.ActionDescriptor.Id;
});
string? identityServer = Configuration.GetValue<string>("IdentityServer:AuthorizationUrl");
c.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl = new Uri($"{identityServer}/connect/authorize"),
TokenUrl = new Uri($"{identityServer}/connect/token"),
Scopes = new Dictionary<string, string>
{
{ "openid", "Your user identifier" },
{ "devicecenter", "Device Center API" }
}
}
}
});
c.MapType<IEnumerable<SortingDescriptor>>(() => new OpenApiSchema { Type = "string", Format = "json" });
c.OperationFilter<SecurityRequirementsOperationFilter>();
c.OperationFilter<CamelCaseNamingOperationFilter>();
});
services.AddTenantMiddleware();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(WebApplication app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Device Center API v1");
c.DocumentTitle = "Device Center API Document";
c.IndexStream = () => GetType().Assembly.GetManifestResourceStream($"{GetType().Assembly.GetName().Name}.Infrastructure.Swagger.Index.html");
c.OAuthClientId("devicecenterswagger");
c.OAuthClientSecret("secret");
c.OAuthAppName("Device Center Swagger");
c.OAuthUsePkce();
});
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseTenantMiddleware();
app.UseAuthorization();
app.MapControllers();
}
}
}

View File

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>125a663f-82cf-4ca0-b1da-506e18c63a40</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\..\Identity\ZeroFramework.IdentityServer.API\Infrastructure\Swagger\CamelCaseNamingOperationFilter.cs" Link="Infrastructure\Swagger\CamelCaseNamingOperationFilter.cs" />
<Compile Include="..\..\Identity\ZeroFramework.IdentityServer.API\Infrastructure\Swagger\SecurityRequirementsOperationFilter.cs" Link="Infrastructure\Swagger\SecurityRequirementsOperationFilter.cs" />
<Compile Include="..\ZeroFramework.DeviceCenter.Infrastructure\EntityFrameworks\DeviceCenterDesignTimeDbContextFactory.cs" Link="Infrastructure\Migration\DeviceCenterDesignTimeDbContextFactory.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="..\..\Identity\ZeroFramework.IdentityServer.API\Infrastructure\Swagger\Index.html" Link="Infrastructure\Swagger\Index.html" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.0" />
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.5" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ZeroFramework.DeviceCenter.Application\ZeroFramework.DeviceCenter.Application.csproj" />
<ProjectReference Include="..\ZeroFramework.DeviceCenter.Infrastructure\ZeroFramework.DeviceCenter.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Infrastructure\Migration\" />
<Folder Include="Infrastructure\Swagger\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,41 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"ConnectionStrings": {
"Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=ZeroFramework.DeviceCenter;Trusted_Connection=True",
"MongoConnectionString": "mongodb://localhost:27017"
},
"EventBus": {
"EventBusRetryCount": 3,
"EventBusConnection": "127.0.0.1",
"SubscriptionClientName": "myeventbus",
"EventBusUserName": "",
"EventBusPassword": ""
},
"AllowedOrigins": [ "http://localhost:8000" ],
"IdentityServer": {
"AuthorizationUrl": "https://localhost:5001"
},
"Tenants": [
{
"TenantId": "5f6f2110-58b6-4cf9-b416-85820ba12c01",
"TenantName": "tenant1",
"ConnectionStrings": {
"Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=ZeroFramework.DeviceCenter;Trusted_Connection=True"
}
},
{
"TenantId": "5f6f2110-58b6-4cf9-b416-85820ba12c02",
"TenantName": "tenant2",
"ConnectionStrings": {
"Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=ZeroFramework.DeviceCenter;Trusted_Connection=True"
}
}
],
"UseDemoLaunchMode": false
}

View File

@ -0,0 +1,34 @@
{
"ConnectionStrings": {
"Default": "Persist Security Info=False;Integrated Security=SSPI;database=ZeroFramework.DeviceCenter;server=(local);TrustServerCertificate=true",
"MongoConnectionString": "mongodb://localhost:27017"
},
"EventBus": {
"EventBusRetryCount": 3,
"EventBusConnection": "127.0.0.1",
"SubscriptionClientName": "myeventbus",
"EventBusUserName": "",
"EventBusPassword": ""
},
"AllowedOrigins": [ "https://cloud.helloworldnet.com" ],
"IdentityServer": {
"AuthorizationUrl": "https://identityserver.helloworldnet.com"
},
"Tenants": [
{
"TenantId": "5f6f2110-58b6-4cf9-b416-85820ba12c01",
"TenantName": "tenant1",
"ConnectionStrings": {
"Default": "Persist Security Info=False;Integrated Security=SSPI;database=ZeroFramework.DeviceCenter;server=(local);TrustServerCertificate=true"
}
},
{
"TenantId": "5f6f2110-58b6-4cf9-b416-85820ba12c02",
"TenantName": "tenant2",
"ConnectionStrings": {
"Default": "Persist Security Info=False;Integrated Security=SSPI;database=ZeroFramework.DeviceCenter;server=(local);TrustServerCertificate=true"
}
}
],
"UseDemoLaunchMode": false
}

View File

@ -0,0 +1,13 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
},
"Console": {
"TimestampFormat": "[HH:mm:ss] "
}
},
"AllowedHosts": "*"
}

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true"
internalLogLevel="Info"
internalLogFile="${basedir}/logs/internal-nlog.txt">
<!-- enable asp.net core layout renderers -->
<extensions>
<add assembly="NLog.Web.AspNetCore"/>
</extensions>
<!-- the targets to write to -->
<targets>
<!-- File Target for all log messages with basic details -->
<target xsi:type="File" name="allfile" fileName="${basedir}/logs/nlog-AspNetCore-all-${shortdate}.log"
layout="${longdate}|${event-properties:item=EventId:whenEmpty=0}|${level:uppercase=true}|${logger}|${message} ${exception:format=tostring}" />
<!-- File Target for own log messages with extra web details using some ASP.NET core renderers -->
<target xsi:type="File" name="ownFile-web" fileName="${basedir}/logs/nlog-AspNetCore-own-${shortdate}.log"
layout="${longdate}|${event-properties:item=EventId:whenEmpty=0}|${level:uppercase=true}|${logger}|${message} ${exception:format=tostring}|url: ${aspnet-request-url}|action: ${aspnet-mvc-action}|${callsite}" />
<!--Console Target for hosting lifetime messages to improve Docker / Visual Studio startup detection -->
<target xsi:type="Console" name="lifetimeConsole" layout="${MicrosoftConsoleLayout}" />
</targets>
<!-- rules to map from logger name to target -->
<rules>
<!--All logs, including from Microsoft-->
<logger name="*" minlevel="Trace" writeTo="allfile" />
<!--Output hosting lifetime messages to console target for faster startup detection -->
<logger name="Microsoft.Hosting.Lifetime" minlevel="Info" writeTo="lifetimeConsole, ownFile-web" final="true" />
<!--Skip non-critical Microsoft logs and so log only own logs (BlackHole) -->
<logger name="Microsoft.*" maxlevel="Info" final="true" />
<logger name="System.Net.Http.*" maxlevel="Info" final="true" />
<logger name="*" minlevel="Trace" writeTo="ownFile-web" />
</rules>
</nlog>

View File

@ -0,0 +1,15 @@
using AutoMapper;
using ZeroFramework.DeviceCenter.Application.Models.Ordering;
using ZeroFramework.DeviceCenter.Domain.Aggregates.OrderAggregate;
namespace ZeroFramework.DeviceCenter.Application.AutoMapper
{
public class BuyersProfile : Profile
{
public BuyersProfile()
{
CreateMap<OrderCreateRequestModel, Order>();
CreateMap<IEnumerable<Order>, Order>();
}
}
}

View File

@ -0,0 +1,23 @@
using AutoMapper;
using ZeroFramework.DeviceCenter.Application.Models.Devices;
using ZeroFramework.DeviceCenter.Domain.Aggregates.DeviceAggregate;
namespace ZeroFramework.DeviceCenter.Application.AutoMapper
{
public class DevicesProfile : Profile
{
public DevicesProfile()
{
AllowNullDestinationValues = true;
AllowNullCollections = true;
CreateMap<Device, DeviceGetResponseModel>();
CreateMap<DeviceCreateRequestModel, Device>();
CreateMap<DeviceUpdateRequestModel, Device>();
CreateMap<DeviceGroup, DeviceGroupGetResponseModel>();
CreateMap<DeviceGroupCreateRequestModel, DeviceGroup>();
CreateMap<DeviceGroupUpdateRequestModel, DeviceGroup>();
}
}
}

View File

@ -0,0 +1,14 @@
using AutoMapper;
using ZeroFramework.DeviceCenter.Application.Models.Measurements;
using ZeroFramework.DeviceCenter.Domain.Aggregates.MeasurementAggregate;
namespace ZeroFramework.DeviceCenter.Application.AutoMapper
{
public class MeasurementsProfile : Profile
{
public MeasurementsProfile()
{
CreateMap<TelemetryAggregate, DevicePropertyReport>();
}
}
}

View File

@ -0,0 +1,16 @@
using AutoMapper;
using ZeroFramework.DeviceCenter.Application.Models.Monitoring;
using ZeroFramework.DeviceCenter.Domain.Aggregates.MonitoringAggregate;
namespace ZeroFramework.DeviceCenter.Application.AutoMapper
{
public class MonitoringProfile : Profile
{
public MonitoringProfile()
{
CreateMap<MonitoringFactor, MonitoringFactorGetResponseModel>();
CreateMap<MonitoringFactorCreateRequestModel, MonitoringFactor>();
CreateMap<MonitoringFactorUpdateRequestModel, MonitoringFactor>();
}
}
}

View File

@ -0,0 +1,22 @@
using AutoMapper;
using ZeroFramework.DeviceCenter.Application.Commands.Products;
using ZeroFramework.DeviceCenter.Application.Models.Products;
using ZeroFramework.DeviceCenter.Domain.Aggregates.ProductAggregate;
namespace ZeroFramework.DeviceCenter.Application.AutoMapper
{
public class ProductsProfile : Profile
{
public ProductsProfile()
{
CreateMap<Product, ProductGetResponseModel>();
CreateMap<ProductCreateRequestModel, Product>();
CreateMap<ProductUpdateRequestModel, Product>();
CreateMap<CreateProductCommand, Product>();
CreateMap<MeasurementUnit, MeasurementUnitGetResponseModel>();
CreateMap<MeasurementUnitCreateRequestModel, MeasurementUnit>();
CreateMap<MeasurementUnitUpdateRequestModel, MeasurementUnit>();
}
}
}

View File

@ -0,0 +1,15 @@
using AutoMapper;
using ZeroFramework.DeviceCenter.Application.Models.Projects;
using ZeroFramework.DeviceCenter.Domain.Aggregates.ProjectAggregate;
namespace ZeroFramework.DeviceCenter.Application.AutoMapper
{
public class ProjectsProfile : Profile
{
public ProjectsProfile()
{
CreateMap<Project, ProjectGetResponseModel>();
CreateMap<ProjectCreateOrUpdateRequestModel, Project>();
}
}
}

View File

@ -0,0 +1,16 @@
using AutoMapper;
using ZeroFramework.DeviceCenter.Application.Models.ResourceGroups;
using ZeroFramework.DeviceCenter.Domain.Aggregates.ResourceGroupAggregate;
namespace ZeroFramework.DeviceCenter.Application.AutoMapper
{
public class ResourceGroupsProfile : Profile
{
public ResourceGroupsProfile()
{
CreateMap<ResourceGroup, ResourceGroupGetResponseModel>();
CreateMap<ResourceGroupCreateRequestModel, ResourceGroup>();
CreateMap<ResourceGroupUpdateRequestModel, ResourceGroup>();
}
}
}

View File

@ -0,0 +1,15 @@
using AutoMapper;
using ZeroFramework.DeviceCenter.Application.Models.Tenants;
using ZeroFramework.DeviceCenter.Domain.Aggregates.TenantAggregate;
namespace ZeroFramework.DeviceCenter.Application.AutoMapper
{
public class TeantsProfile : Profile
{
public TeantsProfile()
{
CreateMap<Tenant, TenantGetResponseModel>();
CreateMap<TenantCreateOrUpdateRequestModel, Tenant>();
}
}
}

View File

@ -0,0 +1,20 @@
using MediatR;
using Microsoft.Extensions.Logging;
using ZeroFramework.EventBus.Extensions;
namespace ZeroFramework.DeviceCenter.Application.Behaviors
{
public class LoggingBehavior<TRequest, TResponse>(ILogger<LoggingBehavior<TRequest, TResponse>> logger) : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>
{
private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger = logger;
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
_logger.LogInformation("Handling command {CommandName} ({@Command})", request.GetGenericTypeName(), request);
var response = await next();
_logger.LogInformation("Command {CommandName} handled - response: {@Response}", request.GetGenericTypeName(), response);
return response;
}
}
}

View File

@ -0,0 +1,35 @@
using MediatR;
using Microsoft.Extensions.Logging;
using System.Transactions;
using ZeroFramework.EventBus.Extensions;
namespace ZeroFramework.DeviceCenter.Application.Behaviors
{
public class TransactionBehavior<TRequest, TResponse>(ILogger<TransactionBehavior<TRequest, TResponse>> logger) : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>
{
private readonly ILogger<TransactionBehavior<TRequest, TResponse>> _logger = logger ?? throw new ArgumentException(nameof(ILogger));
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
string? typeName = request?.GetGenericTypeName();
TResponse? response = default;
using (TransactionScope? scope = new(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
try
{
response = await next();
scope.Complete();
}
catch (Exception ex)
{
_logger.LogError(ex, "ERROR Handling transaction for {CommandName} ({@Command})", typeName, request);
throw;
}
}
return response;
}
}
}

View File

@ -0,0 +1,31 @@
using FluentValidation;
using MediatR;
using Microsoft.Extensions.Logging;
using ZeroFramework.EventBus.Extensions;
namespace ZeroFramework.DeviceCenter.Application.Behaviors
{
public class ValidatorBehavior<TRequest, TResponse>(IEnumerable<IValidator<TRequest>> validators, ILogger<ValidatorBehavior<TRequest, TResponse>> logger) : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>
{
private readonly ILogger<ValidatorBehavior<TRequest, TResponse>> _logger = logger;
private readonly IEnumerable<IValidator<TRequest>> _validators = validators;
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
var typeName = request.GetGenericTypeName();
_logger.LogInformation("Validating command {CommandType}", typeName);
var failures = _validators.Select(v => v.Validate(request)).SelectMany(result => result.Errors).Where(error => error != null).ToList();
if (failures.Any())
{
_logger.LogWarning("Validation errors - {CommandType} - Command: {@Command} - Errors: {@ValidationErrors}", typeName, request, failures);
throw new System.ApplicationException($"Command Validation Errors for type {typeof(TRequest).Name}", new ValidationException("Validation exception", failures));
}
return await next();
}
}
}

View File

@ -0,0 +1,11 @@
using MediatR;
using System.Runtime.Serialization;
namespace ZeroFramework.DeviceCenter.Application.Commands.Ordering
{
public class CancelOrderCommand(Guid orderId) : IRequest<bool>
{
[DataMember]
public Guid OrderId { get; private set; } = orderId;
}
}

View File

@ -0,0 +1,39 @@
using MediatR;
using ZeroFramework.DeviceCenter.Application.Infrastructure;
using ZeroFramework.DeviceCenter.Domain.Aggregates.OrderAggregate;
using ZeroFramework.DeviceCenter.Infrastructure.Idempotency;
namespace ZeroFramework.DeviceCenter.Application.Commands.Ordering
{
public class CancelOrderCommandHandler(IOrderRepository orderRepository) : IRequestHandler<CancelOrderCommand, bool>
{
private readonly IOrderRepository _orderRepository = orderRepository;
/// <summary>
/// Handler which processes the command when customer executes cancel order from app
/// </summary>
public async Task<bool> Handle(CancelOrderCommand command, CancellationToken cancellationToken)
{
var orderToUpdate = await _orderRepository.GetAsync(command.OrderId);
if (orderToUpdate != null)
{
orderToUpdate.SetCancelledStatus();
await _orderRepository.UnitOfWork.SaveChangesAsync(cancellationToken);
}
return await Task.FromResult(true);
}
}
/// <summary>
/// Use for Idempotency in Command process
/// </summary>
public class CancelOrderIdentifiedCommandHandler(IMediator mediator, IRequestManager requestManager) : IdentifiedCommandHandler<CancelOrderCommand, bool>(mediator, requestManager)
{
protected override bool CreateResultForDuplicateRequest()
{
return true; // Ignore duplicate requests for processing order.
}
}
}

View File

@ -0,0 +1,11 @@
using MediatR;
using System.Runtime.Serialization;
namespace ZeroFramework.DeviceCenter.Application.Commands.Ordering
{
public class SetPaidOrderStatusCommand(Guid orderId) : IRequest<bool>
{
[DataMember]
public Guid OrderId { get; private set; } = orderId;
}
}

View File

@ -0,0 +1,32 @@
using MediatR;
using ZeroFramework.DeviceCenter.Domain.Aggregates.OrderAggregate;
namespace ZeroFramework.DeviceCenter.Application.Commands.Ordering
{
public class SetPaidOrderStatusCommandHandler(IOrderRepository orderRepository) : IRequestHandler<SetPaidOrderStatusCommand, bool>
{
private readonly IOrderRepository _orderRepository = orderRepository;
/// <summary>
/// Handler which processes the command when Shipment service confirms the payment
/// </summary>
public async Task<bool> Handle(SetPaidOrderStatusCommand command, CancellationToken cancellationToken)
{
// Simulate a work time for validating the payment
await Task.Delay(10000, cancellationToken);
var orderToUpdate = await _orderRepository.GetAsync(command.OrderId);
if (orderToUpdate == null)
{
return false;
}
orderToUpdate.SetPaidStatus();
await _orderRepository.UnitOfWork.SaveChangesAsync(cancellationToken);
return true;
}
}
}

View File

@ -0,0 +1,27 @@
using MediatR;
using System.Diagnostics.CodeAnalysis;
using ZeroFramework.DeviceCenter.Application.Models.Products;
using ZeroFramework.DeviceCenter.Domain.Aggregates.ProductAggregate;
namespace ZeroFramework.DeviceCenter.Application.Commands.Products
{
public class CreateProductCommand : IRequest<ProductGetResponseModel>
{
[AllowNull]
public string Name { get; set; }
public ProductNodeType NodeType { get; set; }
public ProductNetType NetType { get; set; }
public ProductProtocolType ProtocolType { get; set; }
public ProductDataFormat DataFormat { get; set; }
public ProductFeatures? Features { get; set; }
public string? Remark { get; set; }
public DateTimeOffset CreationTime { get; set; } = DateTimeOffset.Now;
}
}

View File

@ -0,0 +1,29 @@
using AutoMapper;
using MediatR;
using ZeroFramework.DeviceCenter.Application.Infrastructure;
using ZeroFramework.DeviceCenter.Application.Models.Products;
using ZeroFramework.DeviceCenter.Domain.Aggregates.ProductAggregate;
using ZeroFramework.DeviceCenter.Domain.Repositories;
using ZeroFramework.DeviceCenter.Infrastructure.Idempotency;
namespace ZeroFramework.DeviceCenter.Application.Commands.Products
{
public class CreateProductCommandHandler(IRepository<Product> productRepository, IMapper mapper) : IRequestHandler<CreateProductCommand, ProductGetResponseModel>
{
private readonly IRepository<Product> _productRepository = productRepository;
private readonly IMapper _mapper = mapper;
public async Task<ProductGetResponseModel> Handle(CreateProductCommand request, CancellationToken cancellationToken)
{
Product product = _mapper.Map<Product>(request);
product = await _productRepository.InsertAsync(product, true, cancellationToken);
return _mapper.Map<ProductGetResponseModel>(product);
}
}
public class CreateProductIdentifiedCommandHandler(IMediator mediator, IRequestManager requestManager) : IdentifiedCommandHandler<CreateProductCommand, ProductGetResponseModel>(mediator, requestManager)
{
protected override ProductGetResponseModel? CreateResultForDuplicateRequest() => null; // Ignore duplicate requests for processing.
}
}

View File

@ -0,0 +1,170 @@
using FluentValidation;
using MediatR;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Reflection;
using ZeroFramework.DeviceCenter.Application.Behaviors;
using ZeroFramework.DeviceCenter.Application.Infrastructure;
using ZeroFramework.DeviceCenter.Application.Models.Monitoring;
using ZeroFramework.DeviceCenter.Application.Models.Projects;
using ZeroFramework.DeviceCenter.Application.Queries.Factories;
using ZeroFramework.DeviceCenter.Application.Queries.Monitoring;
using ZeroFramework.DeviceCenter.Application.Queries.Ordering;
using ZeroFramework.DeviceCenter.Application.Services.Devices;
using ZeroFramework.DeviceCenter.Application.Services.Generics;
using ZeroFramework.DeviceCenter.Application.Services.Measurements;
using ZeroFramework.DeviceCenter.Application.Services.Ordering;
using ZeroFramework.DeviceCenter.Application.Services.Permissions;
using ZeroFramework.DeviceCenter.Application.Services.Products;
using ZeroFramework.DeviceCenter.Application.Services.Projects;
using ZeroFramework.DeviceCenter.Application.Services.ResourceGroups;
using ZeroFramework.DeviceCenter.Application.Services.Tenants;
using ZeroFramework.DeviceCenter.Domain.Aggregates.MonitoringAggregate;
using ZeroFramework.DeviceCenter.Domain.Repositories;
using ZeroFramework.DeviceCenter.Infrastructure.Constants;
using ZeroFramework.DeviceCenter.Infrastructure.Idempotency;
using ZeroFramework.EventBus;
using ZeroFramework.EventBus.Abstractions;
using ZeroFramework.EventBus.RabbitMQ;
namespace ZeroFramework.DeviceCenter.Application
{
public static class DependencyRegistrar
{
public static IServiceCollection AddApplicationLayer(this IServiceCollection services, IConfiguration configuration)
{
services.AddDomainEvents();
services.AddEventBus(configuration).AddIntegrationEvents();
services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
services.AddQueries(configuration);
services.AddApplicationServices();
services.AddAuthorization();
return services;
}
private static IServiceCollection AddDomainEvents(this IServiceCollection services)
{
// Registers handlers and mediator types from the specified assemblies
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidatorBehavior<,>));
services.AddMediatR(config => config.RegisterServicesFromAssemblies(AppDomain.CurrentDomain.GetAssemblies()));
ValidatorOptions.Global.LanguageManager = new Extensions.Validators.CustomLanguageManager();
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
services.AddTransient<IRequestManager, RequestManager>();
return services;
}
private static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration)
{
services.AddSingleton<IEventBusSubscriptionsManager, InMemoryEventBusSubscriptionsManager>();
IConfigurationSection configurationSection = configuration.GetSection("EventBus");
int retryCount = 5;
if (!string.IsNullOrEmpty(configurationSection["EventBusRetryCount"]))
{
retryCount = int.Parse(configurationSection["EventBusRetryCount"]!);
}
services.AddSingleton<IRabbitMQPersistentConnection>(sp =>
{
var logger = sp.GetRequiredService<ILogger<DefaultRabbitMQPersistentConnection>>();
var factory = new RabbitMQ.Client.ConnectionFactory()
{
HostName = configurationSection["EventBusConnection"],
DispatchConsumersAsync = true
};
if (!string.IsNullOrEmpty(configurationSection["EventBusUserName"]))
{
factory.UserName = configurationSection["EventBusUserName"];
}
if (!string.IsNullOrEmpty(configurationSection["EventBusPassword"]))
{
factory.Password = configurationSection["EventBusPassword"];
}
return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount);
});
services.AddSingleton<IEventBus, EventBusRabbitMQ>(sp =>
{
var rabbitMQPersistentConnection = sp.GetRequiredService<IRabbitMQPersistentConnection>();
var logger = sp.GetRequiredService<ILogger<EventBusRabbitMQ>>();
var eventBusSubcriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>();
string queueName = configurationSection["SubscriptionClientName"]!;
return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, sp, eventBusSubcriptionsManager, queueName, retryCount);
});
return services;
}
private static IServiceCollection AddIntegrationEvents(this IServiceCollection services)
{
var exportedTypes = Assembly.GetExecutingAssembly().ExportedTypes;
var integrationEventHandlers = exportedTypes.Where(t => t.IsAssignableTo(typeof(IIntegrationEventHandler)) && t.IsClass);
integrationEventHandlers.ToList().ForEach(t => services.AddTransient(typeof(IIntegrationEventHandler), t));
services.AddTransient<IIntegrationEventService, IntegrationEventService>();
return services;
}
private static IServiceCollection AddQueries(this IServiceCollection services, IConfiguration configuration)
{
services.AddSingleton<IDbConnectionFactory, DbConnectionFactory>();
string connectionString = configuration.GetConnectionString(DbConstants.DefaultConnectionStringName)!;
services.AddTransient<IOrderQueries>(o => new OrderQueries(connectionString));
services.AddTransient<IMonitoringFactorQueries, MonitoringFactorQueries>();
return services;
}
private static IServiceCollection AddApplicationServices(this IServiceCollection services)
{
var exportedTypes = Assembly.GetExecutingAssembly().ExportedTypes;
var dataSeedProviders = exportedTypes.Where(t => t.IsAssignableTo(typeof(IDataSeedProvider)) && t.IsClass);
dataSeedProviders.ToList().ForEach(t => services.AddTransient(typeof(IDataSeedProvider), t));
services.AddTransient(typeof(ICrudApplicationService<int, ProjectGetResponseModel, PagedRequestModel, ProjectGetResponseModel, ProjectCreateOrUpdateRequestModel, ProjectCreateOrUpdateRequestModel>), typeof(ProjectApplicationService));
services.AddTransient(typeof(ICrudApplicationService<int, MonitoringFactorGetResponseModel, MonitoringFactorPagedRequestModel, MonitoringFactorGetResponseModel, MonitoringFactorCreateRequestModel, MonitoringFactorUpdateRequestModel>), typeof(CrudApplicationService<MonitoringFactor, int, MonitoringFactorGetResponseModel, MonitoringFactorPagedRequestModel, MonitoringFactorGetResponseModel, MonitoringFactorCreateRequestModel, MonitoringFactorUpdateRequestModel>));
services.AddTransient<IOrderApplicationService, OrderApplicationService>();
services.AddTransient<IProductApplicationService, ProductApplicationService>();
services.AddTransient<ITenantApplicationService, TenantApplicationService>();
services.AddTransient<IPermissionApplicationService, PermissionApplicationService>();
services.AddTransient<IResourceGroupApplicationService, ResourceGroupApplicationService>();
services.AddTransient<IMeasurementUnitApplicationService, MeasurementUnitApplicationService>();
services.AddTransient<IDeviceApplicationService, DeviceApplicationService>();
services.AddTransient<IDeviceDataApplicationService, DeviceDataApplicationService>();
services.AddTransient<IDeviceGroupApplicationService, DeviceGroupApplicationService>();
return services;
}
private static IServiceCollection AddAuthorization(this IServiceCollection services)
{
services.AddDistributedMemoryCache().AddTransient<IPermissionStore, PermissionStore>();
services.AddTransient<IPermissionDefinitionManager, PermissionDefinitionManager>();
var exportedTypes = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.ExportedTypes).Where(t => t.IsClass);
var permissionDefinitionProviders = exportedTypes.Where(t => t.IsAssignableTo(typeof(IPermissionDefinitionProvider)));
permissionDefinitionProviders.ToList().ForEach(t => services.AddSingleton(typeof(IPermissionDefinitionProvider), t));
var permissionValueProviders = exportedTypes.Where(t => t.IsAssignableTo(typeof(IPermissionValueProvider)));
permissionValueProviders.ToList().ForEach(t => services.AddTransient(typeof(IPermissionValueProvider), t));
return services;
}
}
}

View File

@ -0,0 +1,30 @@
using MediatR;
using Microsoft.Extensions.Logging;
using ZeroFramework.DeviceCenter.Domain.Aggregates.OrderAggregate;
using ZeroFramework.DeviceCenter.Domain.Events.Buyers;
namespace ZeroFramework.DeviceCenter.Application.DomainEventHandlers.Buyers.BuyerAndPaymentMethodVerified
{
public class UpdateOrderWhenVerifiedDomainEventHandler(IOrderRepository orderRepository, ILoggerFactory loggerFactory) : INotificationHandler<BuyerAndPaymentMethodVerifiedDomainEvent>
{
private readonly IOrderRepository _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
private readonly ILoggerFactory _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
/// <summary>
/// When the Buyer and Buyer's payment method have been created or verified that they existed,
/// then we can update the original Order with the BuyerId and PaymentId (foreign keys)
/// </summary>
public async Task Handle(BuyerAndPaymentMethodVerifiedDomainEvent buyerPaymentMethodVerifiedEvent, CancellationToken cancellationToken)
{
Order orderToUpdate = await _orderRepository.GetAsync(buyerPaymentMethodVerifiedEvent.OrderId);
orderToUpdate.SetBuyerId(buyerPaymentMethodVerifiedEvent.Buyer.Id);
orderToUpdate.SetPaymentMethodId(buyerPaymentMethodVerifiedEvent.Payment.Id);
var logger = _loggerFactory.CreateLogger<UpdateOrderWhenVerifiedDomainEventHandler>();
string message = "Order with Id: {OrderId} has been successfully updated with a payment method {PaymentMethod} ({Id})";
logger.LogTrace(message, buyerPaymentMethodVerifiedEvent.OrderId, nameof(buyerPaymentMethodVerifiedEvent.Payment), buyerPaymentMethodVerifiedEvent.Payment.Id);
}
}
}

View File

@ -0,0 +1,36 @@
using MediatR;
using Microsoft.Extensions.Logging;
using ZeroFramework.DeviceCenter.Application.Infrastructure;
using ZeroFramework.DeviceCenter.Application.IntegrationEvents.Events.Ordering;
using ZeroFramework.DeviceCenter.Domain.Aggregates.BuyerAggregate;
using ZeroFramework.DeviceCenter.Domain.Aggregates.OrderAggregate;
using ZeroFramework.DeviceCenter.Domain.Events.Ordering;
namespace ZeroFramework.DeviceCenter.Application.DomainEventHandlers.Ordering.OrderCancelled
{
public class OrderCancelledDomainEventHandler(IOrderRepository orderRepository, ILoggerFactory loggerFactory, IBuyerRepository buyerRepository, IIntegrationEventService integrationEventService) : INotificationHandler<OrderCancelledDomainEvent>
{
private readonly IOrderRepository _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
private readonly IBuyerRepository _buyerRepository = buyerRepository ?? throw new ArgumentNullException(nameof(buyerRepository));
private readonly ILoggerFactory _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
private readonly IIntegrationEventService _integrationEventService = integrationEventService;
public async Task Handle(OrderCancelledDomainEvent orderCancelledDomainEvent, CancellationToken cancellationToken)
{
_loggerFactory.CreateLogger<OrderCancelledDomainEvent>().LogTrace("Order with Id: {OrderId} has been successfully updated to status {Status} ({Id})", orderCancelledDomainEvent.Order.Id, nameof(OrderStatus.Cancelled), OrderStatus.Cancelled.Id);
var order = await _orderRepository.GetAsync(orderCancelledDomainEvent.Order.Id);
Buyer? buyer = await _buyerRepository.FindByIdAsync(order.BuyerId);
if (buyer is not null)
{
var orderStatusChangedToCancelledIntegrationEvent = new OrderStatusChangedToCancelledIntegrationEvent(order.Id, order.OrderStatus.Name, buyer.UserId);
await _integrationEventService.AddAndSaveEventAsync(orderStatusChangedToCancelledIntegrationEvent);
}
}
}
}

View File

@ -0,0 +1,13 @@
using MediatR;
using ZeroFramework.DeviceCenter.Domain.Events.Ordering;
namespace ZeroFramework.DeviceCenter.Application.DomainEventHandlers.Ordering.OrderCancelled
{
public class SendSmsWhenOrderCancelledDomainEventHandler : INotificationHandler<OrderCancelledDomainEvent>
{
public Task Handle(OrderCancelledDomainEvent notification, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,38 @@
using MediatR;
using System.Transactions;
using ZeroFramework.DeviceCenter.Application.Infrastructure;
using ZeroFramework.DeviceCenter.Application.IntegrationEvents.Events.Ordering;
using ZeroFramework.DeviceCenter.Domain.Aggregates.BuyerAggregate;
using ZeroFramework.DeviceCenter.Domain.Events.Ordering;
namespace ZeroFramework.DeviceCenter.Application.DomainEventHandlers.Ordering.OrderStartedEvent
{
public class AddBuyerWhenOrderStartedDomainEventHandler(IBuyerRepository buyerRepository, IIntegrationEventService integrationEventService) : INotificationHandler<OrderStartedDomainEvent>
{
private readonly IBuyerRepository _buyerRepository = buyerRepository ?? throw new ArgumentNullException(nameof(buyerRepository));
private readonly IIntegrationEventService _integrationEventService = integrationEventService ?? throw new ArgumentNullException(nameof(integrationEventService));
public async Task Handle(OrderStartedDomainEvent orderStartedEvent, CancellationToken cancellationToken)
{
Buyer? buyer = await _buyerRepository.FindAsync(orderStartedEvent.UserId);
bool buyerOriginallyExisted = buyer != null;
buyer ??= new Buyer(userId: orderStartedEvent.UserId);
buyer.VerifyOrAddPaymentMethod(orderStartedEvent.CardNumber, orderStartedEvent.CardTypeId, orderStartedEvent.CardExpiration, orderStartedEvent.Order.Id);
_ = buyerOriginallyExisted ? _buyerRepository.Update(buyer) : _buyerRepository.Add(buyer);
var integrationEvent = new OrderStatusChangedToSubmittedIntegrationEvent(orderStartedEvent.Order.Id, orderStartedEvent.Order.OrderStatus.Name, buyer.UserId);
using var scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted });
await _buyerRepository.UnitOfWork.SaveChangesAsync(cancellationToken);
await _integrationEventService.AddAndSaveEventAsync(integrationEvent);
// Commit transaction if all commands succeed, transaction will auto-rollback
// when disposed if either commands fails
scope.Complete();
}
}
}

View File

@ -0,0 +1,13 @@
using MediatR;
using ZeroFramework.DeviceCenter.Domain.Events.Ordering;
namespace ZeroFramework.DeviceCenter.Application.DomainEventHandlers.Ordering.OrderStartedEvent
{
public class SendEmailWhenOrderStartedDomainEventHandler : INotificationHandler<OrderStartedDomainEvent>
{
public Task Handle(OrderStartedDomainEvent notification, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,86 @@
using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;
namespace ZeroFramework.DeviceCenter.Application.Extensions.Caching
{
public static class CustomDistributedCacheExtensions
{
public static void SetObject<TItem>(this IDistributedCache cache, string key, TItem value)
{
cache.SetObject(key, value, new DistributedCacheEntryOptions());
}
public static void SetObject<TItem>(this IDistributedCache cache, string key, TItem value, DistributedCacheEntryOptions options)
{
cache.SetString(key, JsonSerializer.Serialize(value), options);
}
public static Task SetObjectAsync<TItem>(this IDistributedCache cache, string key, TItem value, CancellationToken token = default)
{
return cache.SetObjectAsync(key, value, new DistributedCacheEntryOptions(), token);
}
public static Task SetObjectAsync<TItem>(this IDistributedCache cache, string key, TItem value, DistributedCacheEntryOptions options, CancellationToken token = default)
{
return cache.SetStringAsync(key, JsonSerializer.Serialize(value), options, token);
}
public static TItem? GetObject<TItem>(this IDistributedCache cache, string key)
{
var dataString = cache.GetString(key);
if (dataString is null)
{
return default;
}
return JsonSerializer.Deserialize<TItem>(dataString);
}
public static async Task<TItem?> GetObjectAsync<TItem>(this IDistributedCache cache, string key, CancellationToken token = default)
{
var dataString = await cache.GetStringAsync(key, token);
if (dataString is null)
{
return default;
}
return JsonSerializer.Deserialize<TItem>(dataString);
}
public static TItem? GetOrCreate<TItem>(this IDistributedCache cache, string key, Func<DistributedCacheEntryOptions, TItem> factory)
{
if (!cache.TryGetValue(key, out TItem? result))
{
var entryOptions = new DistributedCacheEntryOptions();
result = factory(entryOptions);
cache.SetObjectAsync(key, result, entryOptions);
}
return result;
}
public static async Task<TItem?> GetOrCreateAsync<TItem>(this IDistributedCache cache, string key, Func<DistributedCacheEntryOptions, Task<TItem>> factory)
{
if (!cache.TryGetValue(key, out TItem? result))
{
var entryOptions = new DistributedCacheEntryOptions();
result = await factory(entryOptions);
await cache.SetObjectAsync(key, result, entryOptions);
}
return result;
}
public static bool TryGetValue<TItem>(this IDistributedCache cache, string key, out TItem? value)
{
var dataString = cache.GetString(key);
if (dataString is null)
{
value = default;
return false;
}
value = JsonSerializer.Deserialize<TItem>(dataString);
return true;
}
}
}

View File

@ -0,0 +1,66 @@
namespace ZeroFramework.DeviceCenter.Application.Extensions.Validators
{
public class CustomLanguageManager : FluentValidation.Resources.LanguageManager
{
public CustomLanguageManager()
{
AddTranslation("en-US", "EmailValidator", "{PropertyName} is not a valid email address.");
AddTranslation("en-US", "GreaterThanOrEqualValidator", "{PropertyName} must be greater than or equal to {ComparisonValue}.");
AddTranslation("en-US", "GreaterThanValidator", "{PropertyName} must be greater than {ComparisonValue}.");
AddTranslation("en-US", "LengthValidator", "{PropertyName} must be between {MinLength} and {MaxLength} characters. You entered {TotalLength} characters.");
AddTranslation("en-US", "MinimumLengthValidator", "The length of {PropertyName} must be at least {MinLength} characters. You entered {TotalLength} characters.");
AddTranslation("en-US", "MaximumLengthValidator", "The length of {PropertyName} must be {MaxLength} characters or fewer. You entered {TotalLength} characters.");
AddTranslation("en-US", "LessThanOrEqualValidator", "{PropertyName} must be less than or equal to {ComparisonValue}.");
AddTranslation("en-US", "LessThanValidator", "{PropertyName} must be less than {ComparisonValue}.");
AddTranslation("en-US", "NotEmptyValidator", "{PropertyName} must not be empty.");
AddTranslation("en-US", "NotEqualValidator", "{PropertyName} must not be equal to {ComparisonValue}.");
AddTranslation("en-US", "NotNullValidator", "{PropertyName} must not be empty.");
AddTranslation("en-US", "PredicateValidator", "The specified condition was not met for {PropertyName}.");
AddTranslation("en-US", "AsyncPredicateValidator", "The specified condition was not met for {PropertyName}.");
AddTranslation("en-US", "RegularExpressionValidator", "{PropertyName} is not in the correct format.");
AddTranslation("en-US", "EqualValidator", "{PropertyName} must be equal to {ComparisonValue}.");
AddTranslation("en-US", "ExactLengthValidator", "{PropertyName} must be {MaxLength} characters in length. You entered {TotalLength} characters.");
AddTranslation("en-US", "InclusiveBetweenValidator", "{PropertyName} must be between {From} and {To}. You entered {Value}.");
AddTranslation("en-US", "ExclusiveBetweenValidator", "{PropertyName} must be between {From} and {To} (exclusive). You entered {Value}.");
AddTranslation("en-US", "CreditCardValidator", "{PropertyName} is not a valid credit card number.");
AddTranslation("en-US", "ScalePrecisionValidator", "{PropertyName} must not be more than {ExpectedPrecision} digits in total with allowance for {ExpectedScale} decimals. {Digits} digits and {ActualScale} decimals were found.");
AddTranslation("en-US", "EmptyValidator", "{PropertyName} must be empty.");
AddTranslation("en-US", "NullValidator", "{PropertyName} must be empty.");
AddTranslation("en-US", "EnumValidator", "{PropertyName} has a range of values which does not include {PropertyValue}.");
AddTranslation("en-US", "Length_Simple", "{PropertyName} must be between {MinLength} and {MaxLength} characters.");
AddTranslation("en-US", "MinimumLength_Simple", "The length of {PropertyName} must be at least {MinLength} characters.");
AddTranslation("en-US", "MaximumLength_Simple", "The length of {PropertyName} must be {MaxLength} characters or fewer.");
AddTranslation("en-US", "ExactLength_Simple", "{PropertyName} must be {MaxLength} characters in length.");
AddTranslation("en-US", "InclusiveBetween_Simple", "{PropertyName} must be between {From} and {To}.");
AddTranslation("zh-CN", "EmailValidator", "{PropertyName}不是有效的电子邮件地址。");
AddTranslation("zh-CN", "GreaterThanOrEqualValidator", "{PropertyName}必须大于或等于{ComparisonValue}。");
AddTranslation("zh-CN", "GreaterThanValidator", "{PropertyName}必须大于{ComparisonValue}。");
AddTranslation("zh-CN", "LengthValidator", "{PropertyName}的长度必须在{MinLength}到{MaxLength}字符,您输入了{TotalLength}字符。");
AddTranslation("zh-CN", "MinimumLengthValidator", "{PropertyName}必须大于或等于{MinLength}个字符。您输入了{TotalLength}个字符。");
AddTranslation("zh-CN", "MaximumLengthValidator", "{PropertyName}必须小于或等于{MaxLength}个字符。您输入了{TotalLength}个字符。");
AddTranslation("zh-CN", "LessThanOrEqualValidator", "{PropertyName}必须小于或等于{ComparisonValue}。");
AddTranslation("zh-CN", "LessThanValidator", "{PropertyName}必须小于{ComparisonValue}。");
AddTranslation("zh-CN", "NotEmptyValidator", "{PropertyName}不能为空。");
AddTranslation("zh-CN", "NotEqualValidator", "{PropertyName}不能和{ComparisonValue}相等。");
AddTranslation("zh-CN", "NotNullValidator", "{PropertyName}不能为Null。");
AddTranslation("zh-CN", "PredicateValidator", "{PropertyName}不符合指定的条件。");
AddTranslation("zh-CN", "AsyncPredicateValidator", "{PropertyName}不符合指定的条件。");
AddTranslation("zh-CN", "RegularExpressionValidator", "{PropertyName}的格式不正确。");
AddTranslation("zh-CN", "EqualValidator", "{PropertyName}应该和{ComparisonValue}相等。");
AddTranslation("zh-CN", "ExactLengthValidator", "{PropertyName}必须是{MaxLength}个字符,您输入了{TotalLength}字符。");
AddTranslation("zh-CN", "InclusiveBetweenValidator", "{PropertyName}必须在{From}(包含)和{To}(包含)之间,您输入了{Value}。");
AddTranslation("zh-CN", "ExclusiveBetweenValidator", "{PropertyName}必须在{From}(不包含)和{To}(不包含)之间,您输入了{Value}。");
AddTranslation("zh-CN", "CreditCardValidator", "{PropertyName}不是有效的信用卡号。");
AddTranslation("zh-CN", "ScalePrecisionValidator", "{PropertyName}总位数不能超过{ExpectedPrecision}位,其中小数部分{ExpectedScale}位。您共计输入了{Digits}位数字,其中小数部分{ActualScale}位。");
AddTranslation("zh-CN", "EmptyValidator", "{PropertyName}必须为空。");
AddTranslation("zh-CN", "NullValidator", "{PropertyName}必须为Null。");
AddTranslation("zh-CN", "EnumValidator", "{PropertyName}的值范围不包含{PropertyValue}。");
AddTranslation("zh-CN", "Length_Simple", "{PropertyName}的长度必须在{MinLength}到{MaxLength}字符。");
AddTranslation("zh-CN", "MinimumLength_Simple", "{PropertyName}必须大于或等于{MinLength}个字符。");
AddTranslation("zh-CN", "MaximumLength_Simple", "{PropertyName}必须小于或等于{MaxLength}个字符。");
AddTranslation("zh-CN", "ExactLength_Simple", "{PropertyName}必须是{MaxLength}个字符。");
AddTranslation("zh-CN", "InclusiveBetween_Simple", "{PropertyName}必须在{From}(包含)和{To}(包含)之间。");
}
}
}

View File

@ -0,0 +1,11 @@
using ZeroFramework.EventBus.Events;
namespace ZeroFramework.DeviceCenter.Application.Infrastructure
{
public interface IIntegrationEventService
{
Task PublishEventsThroughEventBusAsync(Guid transactionId);
Task AddAndSaveEventAsync(IntegrationEvent evt);
}
}

View File

@ -0,0 +1,11 @@
using MediatR;
namespace ZeroFramework.DeviceCenter.Application.Infrastructure
{
public class IdentifiedCommand<TRequest, TResponse>(TRequest command, string id) : IRequest<TResponse> where TRequest : IRequest<TResponse>
{
public TRequest Command { get; } = command;
public string Id { get; } = id;
}
}

View File

@ -0,0 +1,38 @@
using MediatR;
using ZeroFramework.DeviceCenter.Infrastructure.Idempotency;
namespace ZeroFramework.DeviceCenter.Application.Infrastructure
{
/// <summary>
/// Provides a base implementation for handling duplicate request and ensuring idempotent updates, in the cases where
/// a requestid sent by client is used to detect duplicate requests.
/// </summary>
public class IdentifiedCommandHandler<TRequest, TResponse>(IMediator mediator, IRequestManager requestManager) : IRequestHandler<IdentifiedCommand<TRequest, TResponse>, TResponse> where TRequest : IRequest<TResponse>
{
private readonly IMediator _mediator = mediator;
private readonly IRequestManager _requestManager = requestManager;
/// <summary>
/// Creates the result value to return if a previous request was found
/// </summary>
protected virtual TResponse? CreateResultForDuplicateRequest() => default;
/// <summary>
/// This method handles the command. It just ensures that no other request exists with the same ID, and if this is the case
/// just enqueues the original inner command.
/// </summary>
public async Task<TResponse> Handle(IdentifiedCommand<TRequest, TResponse> command, CancellationToken cancellationToken)
{
if (await _requestManager.ExistAsync(command.Id))
{
return CreateResultForDuplicateRequest() ?? throw new NotImplementedException();
}
await _requestManager.CreateRequestForCommandAsync<TRequest>(command.Id);
// Send the embeded business command to mediator so it runs its related CommandHandler
return await _mediator.Send(command.Command, cancellationToken);
}
}
}

View File

@ -0,0 +1,90 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System.Reflection;
using System.Text.Json;
using ZeroFramework.DeviceCenter.Domain.Repositories;
using ZeroFramework.DeviceCenter.Infrastructure.IntegrationEvents;
using ZeroFramework.EventBus.Abstractions;
using ZeroFramework.EventBus.Events;
namespace ZeroFramework.DeviceCenter.Application.Infrastructure
{
public class IntegrationEventService(IEventBus eventBus, IRepository<IntegrationEventLog> eventLogRepository, ILogger<IntegrationEventService> logger) : IIntegrationEventService
{
private readonly IEventBus _eventBus = eventBus;
private readonly IRepository<IntegrationEventLog> _eventLogRepository = eventLogRepository;
private readonly ILogger<IntegrationEventService> _logger = logger;
private readonly List<Type> _eventTypes = Assembly.GetExecutingAssembly().ExportedTypes.Where(t => t.Name.EndsWith(nameof(IntegrationEvent))).ToList();
public async Task PublishEventsThroughEventBusAsync(Guid transactionId)
{
var tid = transactionId.ToString();
IEnumerable<IntegrationEventLog> result = await _eventLogRepository.Query.Where(e => e.TransactionId == tid && e.Status == IntegrationEventStatus.NotPublished).ToListAsync();
List<IntegrationEvent> pendingLogEvents = [];
if (result != null && result.Any())
{
result = result.OrderBy(o => o.CreationTime);
foreach (var item in result)
{
Type? eventType = _eventTypes.Find(t => t.Name == item.EventTypeShortName);
if (eventType is not null)
{
IntegrationEvent? integrationEvent = JsonSerializer.Deserialize(item.Content, eventType) as IntegrationEvent;
if (integrationEvent is not null)
{
pendingLogEvents.Add(integrationEvent);
}
}
}
}
foreach (var logEvt in pendingLogEvents)
{
_logger.LogInformation("Publishing integration event: {IntegrationEventId} from {AppName} - ({@IntegrationEvent})", logEvt.Id, "ZeroFramework", logEvt);
try
{
await UpdateEventStatus(logEvt.Id, IntegrationEventStatus.InProgress);
await _eventBus.PublishAsync(logEvt);
await UpdateEventStatus(logEvt.Id, IntegrationEventStatus.Published);
}
catch (Exception ex)
{
_logger.LogError(ex, "ERROR publishing integration event: {IntegrationEventId} from {AppName}", logEvt.Id, "ZeroFramework");
await UpdateEventStatus(logEvt.Id, IntegrationEventStatus.PublishedFailed);
}
}
}
public async Task AddAndSaveEventAsync(IntegrationEvent evt)
{
_logger.LogInformation("Enqueuing integration event {IntegrationEventId} to repository ({@IntegrationEvent})", evt.Id, evt);
Guid? transactionId = System.Transactions.Transaction.Current?.TransactionInformation.DistributedIdentifier;
await _eventLogRepository.InsertAsync(new IntegrationEventLog(evt.Id, evt, transactionId), true);
}
private async Task UpdateEventStatus(Guid eventId, IntegrationEventStatus status)
{
IntegrationEventLog eventLog = await _eventLogRepository.GetAsync(ev => ev.Id == eventId);
eventLog.Status = status;
if (status == IntegrationEventStatus.InProgress)
{
eventLog.TimesSent++;
}
await _eventLogRepository.UpdateAsync(eventLog, true);
}
}
}

View File

@ -0,0 +1,33 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace ZeroFramework.DeviceCenter.Application.Infrastructure
{
public class ObjectToInferredTypesConverter : JsonConverter<object?>
{
public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => reader.TokenType switch
{
JsonTokenType.Null => null,
JsonTokenType.True => true,
JsonTokenType.False => false,
JsonTokenType.Number when reader.TryGetInt32(out int value) => value,
JsonTokenType.Number when reader.TryGetInt64(out long value) => value,
JsonTokenType.Number => reader.GetDouble(),
JsonTokenType.String when reader.TryGetDateTimeOffset(out DateTimeOffset dateTimeOffset) => dateTimeOffset,
JsonTokenType.String when reader.TryGetDateTime(out DateTime datetime) => datetime,
JsonTokenType.String => reader.GetString(),
_ => JsonDocument.ParseValue(ref reader).RootElement.Clone()
};
public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options)
{
if (value is not null)
{
JsonSerializer.Serialize(writer, value, value.GetType(), options);
return;
}
writer.WriteNullValue();
}
}
}

View File

@ -0,0 +1,23 @@
namespace ZeroFramework.DeviceCenter.Application.Infrastructure
{
public static class TypeExtensions
{
/// <summary>
/// How To Detect If Type is Another Generic Type
/// </summary>
public static bool IsAssignableToGenericType(this Type givenType, Type genericType)
{
return givenType.GetInterfaces().Any(t => t.IsGenericType && t.GetGenericTypeDefinition() == genericType) || givenType.BaseType != null && (givenType.BaseType.IsGenericType && givenType.BaseType.GetGenericTypeDefinition() == genericType || givenType.BaseType.IsAssignableToGenericType(genericType));
}
/// <summary>
/// How To Detect If Type is Another Generic Type
/// </summary>
public static bool IsAssignableFromGenericType(this Type givenType, Type genericType) => IsAssignableToGenericType(genericType, givenType);
/// <summary>
/// Checks whether this type is a closed type of a given generic type.
/// </summary>
public static bool IsClosedTypeOf(this Type @this, Type openGeneric) => @this.GetInterfaces().Any(t => t.IsGenericType && !@this.ContainsGenericParameters && t.GetGenericTypeDefinition() == openGeneric);
}
}

View File

@ -0,0 +1,18 @@
using MediatR;
using ZeroFramework.DeviceCenter.Application.Commands.Ordering;
using ZeroFramework.DeviceCenter.Application.IntegrationEvents.Events.Ordering;
using ZeroFramework.EventBus.Abstractions;
namespace ZeroFramework.DeviceCenter.Application.IntegrationEvents.EventHandling.Ordering
{
public class OrderPaymentFailedIntegrationEventHandler(IMediator mediator) : IIntegrationEventHandler<OrderPaymentFailedIntegrationEvent>
{
private readonly IMediator _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
public async Task HandleAsync(OrderPaymentFailedIntegrationEvent @event)
{
var command = new CancelOrderCommand(@event.OrderId);
await _mediator.Send(command);
}
}
}

View File

@ -0,0 +1,17 @@
using MediatR;
using ZeroFramework.DeviceCenter.Application.Commands.Ordering;
using ZeroFramework.EventBus.Abstractions;
namespace ZeroFramework.DeviceCenter.Application.IntegrationEvents.EventHandling.Ordering
{
public class OrderPaymentSucceededDynamicIntegrationEventHandler(IMediator mediator) : IDynamicIntegrationEventHandler
{
private readonly IMediator _mediator = mediator;
public async Task HandleAsync(dynamic eventData)
{
var command = new SetPaidOrderStatusCommand(eventData.OrderId);
await _mediator.Send(command);
}
}
}

View File

@ -0,0 +1,18 @@
using MediatR;
using ZeroFramework.DeviceCenter.Application.Commands.Ordering;
using ZeroFramework.DeviceCenter.Application.IntegrationEvents.Events.Ordering;
using ZeroFramework.EventBus.Abstractions;
namespace ZeroFramework.DeviceCenter.Application.IntegrationEvents.EventHandling.Ordering
{
public class OrderPaymentSucceededIntegrationEventHandler(IMediator mediator) : IIntegrationEventHandler<OrderPaymentSucceededIntegrationEvent>
{
private readonly IMediator _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
public async Task HandleAsync(OrderPaymentSucceededIntegrationEvent @event)
{
var command = new SetPaidOrderStatusCommand(@event.OrderId);
await _mediator.Send(command);
}
}
}

View File

@ -0,0 +1,9 @@
using ZeroFramework.EventBus.Events;
namespace ZeroFramework.DeviceCenter.Application.IntegrationEvents.Events.Ordering
{
public class OrderPaymentFailedIntegrationEvent(Guid orderId) : IntegrationEvent
{
public Guid OrderId { get; } = orderId;
}
}

View File

@ -0,0 +1,9 @@
using ZeroFramework.EventBus.Events;
namespace ZeroFramework.DeviceCenter.Application.IntegrationEvents.Events.Ordering
{
public class OrderPaymentSucceededIntegrationEvent(Guid orderId) : IntegrationEvent
{
public Guid OrderId { get; } = orderId;
}
}

View File

@ -0,0 +1,12 @@
using ZeroFramework.EventBus.Events;
namespace ZeroFramework.DeviceCenter.Application.IntegrationEvents.Events.Ordering
{
// Integration Events notes:
// An Event is “something that has happened in the past”, therefore its name has to be
// An Integration Event is an event that can cause side effects to other microsrvices, Bounded-Contexts or external systems.
public class OrderStartedIntegrationEvent(Guid userId) : IntegrationEvent
{
public Guid UserId { get; set; } = userId;
}
}

Some files were not shown because too many files have changed in this diff Show More