1+ using Microsoft . EntityFrameworkCore ;
2+ using Microsoft . Extensions . Caching . Memory ;
3+ using System ;
4+ using System . Collections . Generic ;
5+ using System . Globalization ;
6+ using System . Linq ;
7+ using System . Threading ;
8+ using System . Threading . Tasks ;
9+ using ThingConnect . Pulse . Server . Data ;
10+
11+ namespace ThingConnect . Pulse . Server . Services ;
12+
13+ public sealed class SettingsService : ISettingsService
14+ {
15+ private readonly PulseDbContext _context ;
16+ private readonly IMemoryCache _cache ;
17+ private readonly SemaphoreSlim _semaphore ;
18+ private readonly TimeSpan _cacheExpiration = TimeSpan . FromMinutes ( 5 ) ;
19+
20+ private const string LastRollup15mKey = "last_rollup_15m" ;
21+ private const string LastRollupDailyKey = "last_rollup_daily" ;
22+ private const string LastPruneKey = "last_prune" ;
23+ private const string VersionKey = "version" ;
24+
25+ public SettingsService ( PulseDbContext context , IMemoryCache cache )
26+ {
27+ _context = context ;
28+ _cache = cache ;
29+ _semaphore = new SemaphoreSlim ( 1 , 1 ) ;
30+ }
31+
32+ public async Task < string ? > GetAsync ( string key )
33+ {
34+ var cacheKey = $ "setting:{ key } ";
35+
36+ if ( _cache . TryGetValue ( cacheKey , out string ? cachedValue ) )
37+ {
38+ return cachedValue ;
39+ }
40+
41+ await _semaphore . WaitAsync ( ) ;
42+ try
43+ {
44+ if ( _cache . TryGetValue ( cacheKey , out cachedValue ) )
45+ {
46+ return cachedValue ;
47+ }
48+
49+ var setting = await _context . Settings
50+ . FirstOrDefaultAsync ( s => s . K == key ) ;
51+
52+ var value = setting ? . V ;
53+ _cache . Set ( cacheKey , value , _cacheExpiration ) ;
54+
55+ return value ;
56+ }
57+ finally
58+ {
59+ _semaphore . Release ( ) ;
60+ }
61+ }
62+
63+ public async Task SetAsync ( string key , string value )
64+ {
65+ await _semaphore . WaitAsync ( ) ;
66+ try
67+ {
68+ var setting = await _context . Settings
69+ . FirstOrDefaultAsync ( s => s . K == key ) ;
70+
71+ if ( setting == null )
72+ {
73+ setting = new Setting { K = key , V = value } ;
74+ _context . Settings . Add ( setting ) ;
75+ }
76+ else
77+ {
78+ setting . V = value ;
79+ }
80+
81+ await _context . SaveChangesAsync ( ) ;
82+
83+ var cacheKey = $ "setting:{ key } ";
84+ _cache . Set ( cacheKey , value , _cacheExpiration ) ;
85+ }
86+ finally
87+ {
88+ _semaphore . Release ( ) ;
89+ }
90+ }
91+
92+ public async Task < T ? > GetAsync < T > ( string key ) where T : struct
93+ {
94+ var stringValue = await GetAsync ( key ) ;
95+ if ( string . IsNullOrEmpty ( stringValue ) )
96+ {
97+ return null ;
98+ }
99+
100+ return ConvertToType < T > ( stringValue ) ;
101+ }
102+
103+ public async Task < T ? > GetAsync < T > ( string key , T defaultValue ) where T : struct
104+ {
105+ var result = await GetAsync < T > ( key ) ;
106+ return result ?? defaultValue ;
107+ }
108+
109+ public async Task SetAsync < T > ( string key , T value ) where T : struct
110+ {
111+ var stringValue = ConvertToString ( value ) ;
112+ await SetAsync ( key , stringValue ) ;
113+ }
114+
115+ public async Task < Dictionary < string , string > > GetManyAsync ( params string [ ] keys )
116+ {
117+ var result = new Dictionary < string , string > ( ) ;
118+
119+ foreach ( var key in keys )
120+ {
121+ var value = await GetAsync ( key ) ;
122+ if ( value != null )
123+ {
124+ result [ key ] = value ;
125+ }
126+ }
127+
128+ return result ;
129+ }
130+
131+ public async Task SetManyAsync ( Dictionary < string , string > values )
132+ {
133+ await _semaphore . WaitAsync ( ) ;
134+ try
135+ {
136+ var existingSettings = await _context . Settings
137+ . Where ( s => values . Keys . Contains ( s . K ) )
138+ . ToListAsync ( ) ;
139+
140+ foreach ( var kvp in values )
141+ {
142+ var existing = existingSettings . FirstOrDefault ( s => s . K == kvp . Key ) ;
143+ if ( existing == null )
144+ {
145+ _context . Settings . Add ( new Setting { K = kvp . Key , V = kvp . Value } ) ;
146+ }
147+ else
148+ {
149+ existing . V = kvp . Value ;
150+ }
151+
152+ var cacheKey = $ "setting:{ kvp . Key } ";
153+ _cache . Set ( cacheKey , kvp . Value , _cacheExpiration ) ;
154+ }
155+
156+ await _context . SaveChangesAsync ( ) ;
157+ }
158+ finally
159+ {
160+ _semaphore . Release ( ) ;
161+ }
162+ }
163+
164+ public async Task DeleteAsync ( string key )
165+ {
166+ await _semaphore . WaitAsync ( ) ;
167+ try
168+ {
169+ var setting = await _context . Settings
170+ . FirstOrDefaultAsync ( s => s . K == key ) ;
171+
172+ if ( setting != null )
173+ {
174+ _context . Settings . Remove ( setting ) ;
175+ await _context . SaveChangesAsync ( ) ;
176+ }
177+
178+ var cacheKey = $ "setting:{ key } ";
179+ _cache . Remove ( cacheKey ) ;
180+ }
181+ finally
182+ {
183+ _semaphore . Release ( ) ;
184+ }
185+ }
186+
187+ public async Task < bool > ExistsAsync ( string key )
188+ {
189+ var value = await GetAsync ( key ) ;
190+ return value != null ;
191+ }
192+
193+ public async Task < DateTimeOffset ? > GetLastRollup15mTimestampAsync ( )
194+ {
195+ return await GetAsync < DateTimeOffset > ( LastRollup15mKey ) ;
196+ }
197+
198+ public async Task SetLastRollup15mTimestampAsync ( DateTimeOffset timestamp )
199+ {
200+ await SetAsync ( LastRollup15mKey , timestamp ) ;
201+ }
202+
203+ public async Task < DateOnly ? > GetLastRollupDailyDateAsync ( )
204+ {
205+ return await GetAsync < DateOnly > ( LastRollupDailyKey ) ;
206+ }
207+
208+ public async Task SetLastRollupDailyDateAsync ( DateOnly date )
209+ {
210+ await SetAsync ( LastRollupDailyKey , date ) ;
211+ }
212+
213+ public async Task < DateTimeOffset ? > GetLastPruneTimestampAsync ( )
214+ {
215+ return await GetAsync < DateTimeOffset > ( LastPruneKey ) ;
216+ }
217+
218+ public async Task SetLastPruneTimestampAsync ( DateTimeOffset timestamp )
219+ {
220+ await SetAsync ( LastPruneKey , timestamp ) ;
221+ }
222+
223+ public async Task < string ? > GetVersionAsync ( )
224+ {
225+ return await GetAsync ( VersionKey ) ;
226+ }
227+
228+ public async Task SetVersionAsync ( string version )
229+ {
230+ await SetAsync ( VersionKey , version ) ;
231+ }
232+
233+ private static T ? ConvertToType < T > ( string value ) where T : struct
234+ {
235+ try
236+ {
237+ if ( typeof ( T ) == typeof ( DateTimeOffset ) )
238+ {
239+ if ( DateTimeOffset . TryParse ( value , null , DateTimeStyles . RoundtripKind , out var dateTimeOffset ) )
240+ {
241+ return ( T ) ( object ) dateTimeOffset ;
242+ }
243+ }
244+ else if ( typeof ( T ) == typeof ( DateOnly ) )
245+ {
246+ if ( DateOnly . TryParse ( value , out var dateOnly ) )
247+ {
248+ return ( T ) ( object ) dateOnly ;
249+ }
250+ }
251+ else if ( typeof ( T ) == typeof ( int ) )
252+ {
253+ if ( int . TryParse ( value , out var intValue ) )
254+ {
255+ return ( T ) ( object ) intValue ;
256+ }
257+ }
258+ else if ( typeof ( T ) == typeof ( long ) )
259+ {
260+ if ( long . TryParse ( value , out var longValue ) )
261+ {
262+ return ( T ) ( object ) longValue ;
263+ }
264+ }
265+ else if ( typeof ( T ) == typeof ( bool ) )
266+ {
267+ if ( bool . TryParse ( value , out var boolValue ) )
268+ {
269+ return ( T ) ( object ) boolValue ;
270+ }
271+ }
272+ else if ( typeof ( T ) == typeof ( double ) )
273+ {
274+ if ( double . TryParse ( value , out var doubleValue ) )
275+ {
276+ return ( T ) ( object ) doubleValue ;
277+ }
278+ }
279+ }
280+ catch
281+ {
282+ // Return null on any conversion failure
283+ }
284+
285+ return null ;
286+ }
287+
288+ private static string ConvertToString < T > ( T value ) where T : struct
289+ {
290+ if ( typeof ( T ) == typeof ( DateTimeOffset ) )
291+ {
292+ return ( ( DateTimeOffset ) ( object ) value ) . ToString ( "O" ) ;
293+ }
294+ else if ( typeof ( T ) == typeof ( DateOnly ) )
295+ {
296+ return ( ( DateOnly ) ( object ) value ) . ToString ( "yyyy-MM-dd" ) ;
297+ }
298+
299+ return value . ToString ( ) ?? string . Empty ;
300+ }
301+ }
0 commit comments