├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── bug_report.md ├── workflows │ ├── publish-core.yml │ ├── publish-data.yml │ ├── publish-logging.yml │ ├── publish-data.sqlbulkops.yml │ ├── ci-all.yml │ ├── lib-publish-nuget-org.yml │ ├── codeql.yml │ └── lib-ci.yml ├── stale.yml └── dependabot.yml ├── swan-logo-32.png ├── swan-logo-256.png ├── src ├── Swan.Data │ ├── Data │ │ ├── Providers │ │ │ ├── MySqlTypeMapper.cs │ │ │ ├── SqlServerTypeMapper.cs │ │ │ ├── SqliteTypeMapper.cs │ │ │ ├── SqliteDbProvider.cs │ │ │ ├── MySqlDbProvider.cs │ │ │ ├── SqliteColumn.cs │ │ │ ├── SqlServerDbProvider.cs │ │ │ └── SqlServerColumn.cs │ │ ├── IDbConnected.cs │ │ ├── Schema │ │ │ ├── TableIdentifier.cs │ │ │ ├── DbColumnSchema.cs │ │ │ ├── IDbTableSchema.cs │ │ │ └── ITableBuilder.cs │ │ ├── IDbTypeMapper.cs │ │ ├── Extensions │ │ │ └── DataTableExtensions.cs │ │ ├── Records │ │ │ └── PropertyColumnMap.cs │ │ └── DbProviders.cs │ ├── GlobalUsings.cs │ └── Swan.Data.csproj ├── Swan.Core │ ├── Mapping │ │ ├── CopyableAttribute.cs │ │ └── ObjectMap.cs │ ├── GlobalUsings.cs │ ├── Formatters │ │ ├── CsvMapping.cs │ │ ├── CsvReader.cs │ │ └── JsonFormatter.Dynamic.cs │ ├── Threading │ │ ├── AtomicDateTime.cs │ │ ├── AtomicLong.cs │ │ ├── AtomicTimeSpan.cs │ │ ├── AtomicBoolean.cs │ │ ├── AtomicInteger.cs │ │ ├── AtomicDouble.cs │ │ ├── AtomicEnum.cs │ │ └── IWaitEvent.cs │ ├── Collections │ │ └── CollectionEnumerator.cs │ ├── Reflection │ │ ├── CollectionKind.cs │ │ ├── TypeManager.Definitions.cs │ │ ├── ICollectionInfo.cs │ │ ├── IPropertyProxy.cs │ │ ├── TypeManager.Properties.cs │ │ └── TypeManager.Collections.cs │ ├── Gizmos │ │ ├── AsyncLazy.cs │ │ ├── SlidingWindowRateLimiter.cs │ │ ├── Paginator.cs │ │ ├── RangeExtensions.cs │ │ ├── RangeLookup.cs │ │ └── ValueCache.cs │ ├── Swan.Core.csproj │ └── Platform │ │ └── SwanRuntime.cs ├── Swan.Data.SqlBulkOps │ ├── GlobalUsings.cs │ ├── Constants.cs │ └── Swan.Data.SqlBulkOps.csproj ├── Swan.Logging │ ├── Logging │ │ ├── ILogger.cs │ │ ├── LogLevel.cs │ │ ├── DebugLogger.cs │ │ └── TextLogger.cs │ ├── Platform │ │ ├── TerminalWriterFlags.cs │ │ ├── Terminal.Graphics.cs │ │ └── Terminal.Settings.cs │ └── Swan.Logging.csproj ├── Swan.Net │ ├── ConnectionDataReceivedTrigger.cs │ ├── Smtp │ │ ├── SmtpDefinitions.cs │ │ ├── SmtpSender.cs │ │ └── Enums.Smtp.cs │ ├── Swan.Net.csproj │ ├── JsonRequestException.cs │ ├── Extensions │ │ └── NetworkExtensions.cs │ └── Eventing.cs ├── Swan.Samples │ ├── Properties │ │ └── PublishProfiles │ │ │ └── Linux-Arm.pubxml │ ├── Swan.Samples.csproj │ └── Models.cs ├── Swan.Threading │ ├── Threading │ │ ├── ISyncLocker.cs │ │ ├── WorkerState.cs │ │ ├── IWorkerDelayProvider.cs │ │ ├── CancellationTokenOwner.cs │ │ └── IWorker.cs │ └── Swan.Threading.csproj ├── Swan.Artifacts │ ├── Utilities │ │ └── ProcessResult.cs │ ├── Swan.Artifacts.csproj │ ├── Extensions │ │ └── ArtifactExtensions.cs │ └── Threading │ │ └── PeriodicTask.cs └── Swan.Lite │ ├── Swan.Lite.csproj │ ├── Collections │ └── CollectionCacheRepository.cs │ └── SingletonBase.cs ├── test └── Swan.Test │ ├── Mocks │ ├── MethodCacheMock.cs │ ├── LargeObject.cs │ ├── ObjectMapperMock.cs │ ├── ContainerMock.cs │ ├── AppWorkerMock.cs │ ├── CollectionSamples.cs │ └── ProjectRecord.cs │ ├── GlobalUsings.cs │ ├── CurrentAppTest.cs │ ├── DebugLoggerTests.cs │ ├── ExtensionsDictionariesTest.cs │ ├── ExtensionsDatesTest.cs │ ├── RangeExtensionsTest.cs │ ├── DelayProviderTest.cs │ ├── AsyncLazyTest.cs │ ├── TestFixtureBase.cs │ ├── ThreadWorkerBaseTest.cs │ ├── ExtensionsValueTypeTest.cs │ ├── WaitEventFactoryTest.cs │ ├── FileLoggerTest.cs │ ├── TerminalTest.cs │ ├── JsonTest.cs │ ├── JsonFormatterDynamicTest.cs │ ├── PaginatorTest.cs │ ├── Swan.Test.csproj │ ├── AppWorkerBaseTest.cs │ ├── JsonFormatterTest.cs │ ├── TableContextTest.cs │ ├── CsvDynamicReaderTest.cs │ ├── TextSerializerTest.cs │ ├── CsvDictionaryReaderTest.cs │ ├── CodeGenExtensionTests.cs │ └── CsvWriterGenericTest.cs ├── StyleCop.Analyzers.ruleset ├── README.md ├── LICENSE ├── .gitattributes └── CODE_OF_CONDUCT.md /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /swan-logo-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unosquare/swan/HEAD/swan-logo-32.png -------------------------------------------------------------------------------- /swan-logo-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unosquare/swan/HEAD/swan-logo-256.png -------------------------------------------------------------------------------- /src/Swan.Data/Data/Providers/MySqlTypeMapper.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Data.Providers; 2 | 3 | internal class MySqlTypeMapper : DbTypeMapper 4 | { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /src/Swan.Data/Data/Providers/SqlServerTypeMapper.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Data.Providers; 2 | 3 | internal class SqlServerTypeMapper : DbTypeMapper 4 | { 5 | // placeholder 6 | } 7 | -------------------------------------------------------------------------------- /test/Swan.Test/Mocks/MethodCacheMock.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test.Mocks; 2 | 3 | internal class MethodCacheMock 4 | { 5 | public static Task GetMethodTest(string value) => Task.FromResult(default(T)); 6 | } 7 | -------------------------------------------------------------------------------- /test/Swan.Test/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // Global using directives 2 | 3 | global using Microsoft.Data.Sqlite; 4 | global using NUnit.Framework; 5 | global using Swan.Data; 6 | global using Swan.Data.Extensions; 7 | global using Swan.Data.Schema; 8 | -------------------------------------------------------------------------------- /test/Swan.Test/CurrentAppTest.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test; 2 | 3 | using Platform; 4 | 5 | [TestFixture] 6 | public class CurrentAppTest 7 | { 8 | [Test] 9 | public void IsSingleInstanceTest() => Assert.IsTrue(SwanRuntime.IsTheOnlyInstance); 10 | } 11 | -------------------------------------------------------------------------------- /test/Swan.Test/Mocks/LargeObject.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test.Mocks; 2 | 3 | public class LargeObject 4 | { 5 | public int InitializedBy { get; } 6 | 7 | public LargeObject(int initializedBy) 8 | { 9 | InitializedBy = initializedBy; 10 | } 11 | 12 | public long[] Data = new long[100000000]; 13 | } 14 | -------------------------------------------------------------------------------- /src/Swan.Core/Mapping/CopyableAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Mapping; 2 | 3 | /// 4 | /// Represents an attribute to select which properties are copyable between objects. 5 | /// 6 | /// 7 | [AttributeUsage(AttributeTargets.Property)] 8 | public sealed class CopyableAttribute : Attribute 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/publish-core.yml: -------------------------------------------------------------------------------- 1 | name: Publish Swan.Core to Nuget.org 2 | on: workflow_dispatch 3 | 4 | jobs: 5 | publish-nuget: 6 | uses: unosquare/swan/.github/workflows/lib-publish-nuget-org.yml@master 7 | with: 8 | source-path: 'src/' 9 | project-name: 'Swan.Core' 10 | secrets: 11 | nuget-api-key: ${{ secrets.NUGET_API_KEY }} 12 | -------------------------------------------------------------------------------- /.github/workflows/publish-data.yml: -------------------------------------------------------------------------------- 1 | name: Publish Swan.Data to Nuget.org 2 | on: workflow_dispatch 3 | 4 | jobs: 5 | publish-nuget: 6 | uses: unosquare/swan/.github/workflows/lib-publish-nuget-org.yml@master 7 | with: 8 | source-path: 'src/' 9 | project-name: 'Swan.Data' 10 | secrets: 11 | nuget-api-key: ${{ secrets.NUGET_API_KEY }} 12 | -------------------------------------------------------------------------------- /.github/workflows/publish-logging.yml: -------------------------------------------------------------------------------- 1 | name: Publish Swan.Logging to Nuget.org 2 | on: workflow_dispatch 3 | 4 | jobs: 5 | publish-nuget: 6 | uses: unosquare/swan/.github/workflows/lib-publish-nuget-org.yml@master 7 | with: 8 | source-path: 'src/' 9 | project-name: 'Swan.Logging' 10 | secrets: 11 | nuget-api-key: ${{ secrets.NUGET_API_KEY }} 12 | -------------------------------------------------------------------------------- /src/Swan.Core/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable IDE0065 // Misplaced using directive 2 | global using System.Collections; 3 | global using System.Collections.Concurrent; 4 | global using System.Diagnostics.CodeAnalysis; 5 | global using System.Globalization; 6 | global using System.Reflection; 7 | global using System.Text; 8 | #pragma warning restore IDE0065 // Misplaced using directive 9 | -------------------------------------------------------------------------------- /.github/workflows/publish-data.sqlbulkops.yml: -------------------------------------------------------------------------------- 1 | name: Publish Swan.Data.SqlBulkOps to Nuget.org 2 | on: workflow_dispatch 3 | 4 | jobs: 5 | publish-nuget: 6 | uses: unosquare/swan/.github/workflows/lib-publish-nuget-org.yml@master 7 | with: 8 | source-path: 'src/' 9 | project-name: 'Swan.Data.SqlBulkOps' 10 | secrets: 11 | nuget-api-key: ${{ secrets.NUGET_API_KEY }} 12 | -------------------------------------------------------------------------------- /src/Swan.Data.SqlBulkOps/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // TODO: Using MERGE in TSQL for table merging 2 | // TODO: replace temp tables with memory optimized tables 3 | // TODO: For trasnactions, used Delayed_durability 4 | 5 | global using Swan.Data.Context; 6 | global using Swan.Data.Extensions; 7 | global using Swan.Extensions; 8 | global using System.Collections; 9 | global using System.Data.Common; 10 | global using System.Data.SqlClient; 11 | 12 | [assembly: CLSCompliant(true)] 13 | -------------------------------------------------------------------------------- /test/Swan.Test/DebugLoggerTests.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test; 2 | 3 | using Logging; 4 | 5 | [TestFixture] 6 | public class DebugLoggerTests 7 | { 8 | [Test] 9 | public void DebugLoggerTest() => 10 | Assert.That(DebugLogger.Instance, Is.Not.Null); 11 | 12 | [Test] 13 | public void LogLevelTest() => 14 | Assert.That(DebugLogger.Instance.LogLevel, 15 | Is.EqualTo(DebugLogger.IsDebuggerAttached ? LogLevel.Trace : LogLevel.None)); 16 | } 17 | -------------------------------------------------------------------------------- /src/Swan.Data/Data/IDbConnected.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Data; 2 | 3 | /// 4 | /// Implemented in classes that keep connection references. 5 | /// 6 | public interface IDbConnected 7 | { 8 | /// 9 | /// Gets the live connection associated with this object. 10 | /// 11 | DbConnection Connection { get; } 12 | 13 | /// 14 | /// Gets the associated . 15 | /// 16 | DbProvider Provider { get; } 17 | } 18 | -------------------------------------------------------------------------------- /src/Swan.Data/Data/Providers/SqliteTypeMapper.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Data.Providers; 2 | 3 | internal class SqliteTypeMapper : DbTypeMapper 4 | { 5 | public SqliteTypeMapper() 6 | { 7 | ProviderTypeMap[typeof(double)] = "DOUBLE"; 8 | ProviderTypeMap[typeof(bool)] = "BOOLEAN"; 9 | ProviderTypeMap[typeof(Guid)] = "TEXT"; 10 | ProviderTypeMap[typeof(DateTime)] = "DATETIME"; 11 | ProviderTypeMap[typeof(DateTimeOffset)] = "DATETIME"; 12 | ProviderTypeMap[typeof(byte[])] = "BLOB"; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/Swan.Test/ExtensionsDictionariesTest.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test; 2 | 3 | [TestFixture] 4 | public class GetValueOrDefault : TestFixtureBase 5 | { 6 | [Test] 7 | public void NullDictionary_ThrowsArgumentNullException() => Assert.Throws(() => NullDict.GetValueOrDefault(1)); 8 | 9 | [Test] 10 | public void DictionaryWithExistingKey_ReturnsValue() => Assert.AreEqual(DefaultDictionary.GetValueOrDefault(3), "C"); 11 | 12 | [Test] 13 | public void DictionaryWithoutExistingKey_ReturnsNull() => Assert.IsNull(DefaultDictionary.GetValueOrDefault(7)); 14 | } 15 | -------------------------------------------------------------------------------- /test/Swan.Test/ExtensionsDatesTest.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test; 2 | 3 | using Extensions; 4 | 5 | [TestFixture] 6 | public class ToDateTime 7 | { 8 | [TestCase(null)] 9 | [TestCase("")] 10 | [TestCase(" ")] 11 | public void InvalidArguments_ThrowsArgumentNullException(string date) => Assert.Throws(() => date.ToDateTime()); 12 | 13 | [TestCase("2017 10 26")] 14 | [TestCase("2017-10")] 15 | [TestCase("2017-10-26 15:35")] 16 | public void DatesNotParseable_ThrowsException(string date) => Assert.Throws(() => date.ToDateTime()); 17 | } 18 | -------------------------------------------------------------------------------- /test/Swan.Test/RangeExtensionsTest.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test; 2 | 3 | using NUnit.Framework; 4 | using Gizmos; 5 | 6 | [TestFixture] 7 | public class RangeExtensionsTest 8 | { 9 | [Test] 10 | public void WithRangeLookUp_GetsClosestToRequestedValue() 11 | { 12 | var range = new RangeLookup 13 | { 14 | { 0 , "zero" }, 15 | { 1 , "one" }, 16 | { 4 , "four" }, 17 | { 5 , "five" } 18 | }; 19 | range.TryGetValue(2, out string outValue); 20 | 21 | Assert.AreEqual(range.FirstOrDefault(keys => keys.Key == 1).Value, outValue); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Swan.Data/Data/Schema/TableIdentifier.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Data.Schema; 2 | 3 | /// 4 | /// Represents a table and schema name. 5 | /// 6 | /// The table name. 7 | /// The schema name this table belongs to. 8 | /// Sets whether the table is a view. 9 | public sealed record TableIdentifier(string Name, string Schema, bool IsView) 10 | { 11 | /// 12 | /// Creates a new instance of the record. 13 | /// 14 | public TableIdentifier() : this(string.Empty, string.Empty, false) { } 15 | } 16 | -------------------------------------------------------------------------------- /test/Swan.Test/DelayProviderTest.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test; 2 | 3 | using Threading; 4 | 5 | [TestFixture] 6 | public class DelayProviderTest 7 | { 8 | [Test] 9 | [TestCase(DelayProvider.DelayStrategy.ThreadSleep)] 10 | [TestCase(DelayProvider.DelayStrategy.TaskDelay)] 11 | [TestCase(DelayProvider.DelayStrategy.ThreadPool)] 12 | public void WaitOne_TakesCertainTime(DelayProvider.DelayStrategy strategy) 13 | { 14 | using var delay = new DelayProvider(strategy); 15 | var time = delay.WaitOne(); 16 | var mil = time.Milliseconds; 17 | Assert.GreaterOrEqual(mil, 1, $"Strategy {strategy}"); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Swan.Logging/Logging/ILogger.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Logging; 2 | 3 | /// 4 | /// Interface for a logger implementation. 5 | /// 6 | public interface ILogger : IDisposable 7 | { 8 | /// 9 | /// Gets the log level. 10 | /// 11 | /// 12 | /// The log level. 13 | /// 14 | LogLevel LogLevel { get; } 15 | 16 | /// 17 | /// Logs the specified log event. 18 | /// 19 | /// The instance containing the event data. 20 | void Log(LogMessageReceivedEventArgs logEvent); 21 | } 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /test/Swan.Test/Mocks/ObjectMapperMock.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test.Mocks; 2 | 3 | public class User 4 | { 5 | public string? Name { get; set; } 6 | 7 | public string? Email { get; set; } 8 | 9 | public Role? Role { get; set; } 10 | 11 | public DateTime StartDate { get; set; } 12 | } 13 | 14 | public class Role 15 | { 16 | public string? Name { get; set; } 17 | } 18 | 19 | public class UserDto 20 | { 21 | public string? Name { get; set; } 22 | 23 | public string? Email { get; set; } 24 | 25 | public string? Role { get; set; } 26 | 27 | public bool IsAdmin { get; set; } 28 | 29 | public DateTime StartDate { get; set; } 30 | } 31 | -------------------------------------------------------------------------------- /src/Swan.Net/ConnectionDataReceivedTrigger.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Net; 2 | 3 | /// 4 | /// Enumerates the possible causes of the DataReceived event occurring. 5 | /// 6 | public enum ConnectionDataReceivedTrigger 7 | { 8 | /// 9 | /// The trigger was a forceful flush of the buffer 10 | /// 11 | Flush, 12 | 13 | /// 14 | /// The new line sequence bytes were received 15 | /// 16 | NewLineSequenceEncountered, 17 | 18 | /// 19 | /// The buffer was full 20 | /// 21 | BufferFull, 22 | 23 | /// 24 | /// The block size reached 25 | /// 26 | BlockSizeReached, 27 | } 28 | -------------------------------------------------------------------------------- /test/Swan.Test/AsyncLazyTest.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test; 2 | 3 | using NUnit.Framework; 4 | using Gizmos; 5 | using Mocks; 6 | 7 | [TestFixture] 8 | public class AsyncLazyTest 9 | { 10 | static AsyncLazy? lazyLargeObject; 11 | 12 | static async Task InitLargeObject() 13 | { 14 | Random random = new Random(); 15 | await Task.Delay(100); 16 | return new(random.Next()); 17 | } 18 | 19 | [Test] 20 | public async Task WithLargObject_getsValueAsync() 21 | { 22 | lazyLargeObject = new(InitLargeObject); 23 | 24 | var large = await lazyLargeObject.GetValueAsync(); 25 | 26 | Assert.IsTrue(lazyLargeObject.IsValueCreated); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Swan.Logging/Platform/TerminalWriterFlags.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Platform; 2 | 3 | using System; 4 | 5 | /// 6 | /// Defines a set of bitwise standard terminal writers. 7 | /// 8 | [Flags] 9 | public enum TerminalWriterFlags 10 | { 11 | /// 12 | /// Prevents output 13 | /// 14 | None = 0, 15 | 16 | /// 17 | /// Writes to the Console.Out 18 | /// 19 | StandardOutput = 1, 20 | 21 | /// 22 | /// Writes to the Console.Error 23 | /// 24 | StandardError = 2, 25 | 26 | /// 27 | /// Writes to all possible terminal writers 28 | /// 29 | All = StandardOutput | StandardError, 30 | } 31 | -------------------------------------------------------------------------------- /src/Swan.Core/Formatters/CsvMapping.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Formatters; 2 | 3 | internal class CsvMapping 4 | { 5 | internal delegate void ApplyMapping(CsvMapping mapping, TDestination instance); 6 | 7 | public CsvMapping(TContainer container, string heading, string targetName, ApplyMapping apply) 8 | { 9 | Heading = heading; 10 | TargetName = targetName; 11 | Container = container; 12 | Apply = apply; 13 | } 14 | 15 | public string Heading { get; } 16 | 17 | public string TargetName { get; } 18 | 19 | public TContainer Container { get; } 20 | 21 | public ApplyMapping Apply { get; } 22 | } 23 | -------------------------------------------------------------------------------- /src/Swan.Samples/Properties/PublishProfiles/Linux-Arm.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | FileSystem 8 | Debug 9 | Any CPU 10 | netcoreapp2.2 11 | C:\projects\swan\src\Swan.Samples\bin\Debug\netcoreapp2.2\linux-arm\publish 12 | linux-arm 13 | false 14 | <_IsPortable>true 15 | 16 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /src/Swan.Threading/Threading/ISyncLocker.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Threading; 2 | 3 | /// 4 | /// Defines a generic interface for synchronized locking mechanisms. 5 | /// 6 | public interface ISyncLocker : IDisposable 7 | { 8 | /// 9 | /// Acquires a writer lock. 10 | /// The lock is released when the returned locking object is disposed. 11 | /// 12 | /// A disposable locking object. 13 | IDisposable AcquireWriterLock(); 14 | 15 | /// 16 | /// Acquires a reader lock. 17 | /// The lock is released when the returned locking object is disposed. 18 | /// 19 | /// A disposable locking object. 20 | IDisposable AcquireReaderLock(); 21 | } 22 | -------------------------------------------------------------------------------- /src/Swan.Core/Threading/AtomicDateTime.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Threading; 2 | 3 | /// 4 | /// Defines an atomic DateTime. 5 | /// 6 | public sealed class AtomicDateTime : AtomicTypeBase 7 | { 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | /// The initial value. 12 | public AtomicDateTime(DateTime initialValue) 13 | : base(initialValue.Ticks) 14 | { 15 | // placeholder 16 | } 17 | 18 | /// 19 | protected override DateTime FromLong(long backingValue) => new(backingValue); 20 | 21 | /// 22 | protected override long ToLong(DateTime value) => value.Ticks; 23 | } 24 | -------------------------------------------------------------------------------- /src/Swan.Core/Threading/AtomicLong.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Threading; 2 | 3 | /// 4 | /// Fast, atomic long combining interlocked to write value and volatile to read values. 5 | /// 6 | public sealed class AtomicLong : AtomicTypeBase 7 | { 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | /// if set to true [initial value]. 12 | public AtomicLong(long initialValue = default) 13 | : base(initialValue) 14 | { 15 | // placeholder 16 | } 17 | 18 | /// 19 | protected override long FromLong(long backingValue) => backingValue; 20 | 21 | /// 22 | protected override long ToLong(long value) => value; 23 | } 24 | -------------------------------------------------------------------------------- /src/Swan.Core/Threading/AtomicTimeSpan.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Threading; 2 | 3 | /// 4 | /// Represents an atomic TimeSpan type. 5 | /// 6 | public sealed class AtomicTimeSpan : AtomicTypeBase 7 | { 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | /// The initial value. 12 | public AtomicTimeSpan(TimeSpan initialValue) 13 | : base(initialValue.Ticks) 14 | { 15 | // placeholder 16 | } 17 | 18 | /// 19 | protected override TimeSpan FromLong(long backingValue) => TimeSpan.FromTicks(backingValue); 20 | 21 | /// 22 | protected override long ToLong(TimeSpan value) => value.Ticks < 0 ? 0 : value.Ticks; 23 | } 24 | -------------------------------------------------------------------------------- /src/Swan.Threading/Threading/WorkerState.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Threading; 2 | 3 | /// 4 | /// Enumerates the different states in which a worker can be. 5 | /// 6 | public enum WorkerState 7 | { 8 | /// 9 | /// The worker has been created and it is ready to start. 10 | /// 11 | Created, 12 | 13 | /// 14 | /// The worker is running it cycle logic. 15 | /// 16 | Running, 17 | 18 | /// 19 | /// The worker is running its delay logic. 20 | /// 21 | Waiting, 22 | 23 | /// 24 | /// The worker is in the paused or suspended state. 25 | /// 26 | Paused, 27 | 28 | /// 29 | /// The worker is stopped and ready for disposal. 30 | /// 31 | Stopped, 32 | } 33 | -------------------------------------------------------------------------------- /StyleCop.Analyzers.ruleset: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Swan.Core/Threading/AtomicBoolean.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Threading; 2 | 3 | /// 4 | /// Fast, atomic boolean combining interlocked to write value and volatile to read values. 5 | /// 6 | public sealed class AtomicBoolean : AtomicTypeBase 7 | { 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | /// if set to true [initial value]. 12 | public AtomicBoolean(bool initialValue = default) 13 | : base(initialValue ? 1 : 0) 14 | { 15 | // placeholder 16 | } 17 | 18 | /// 19 | protected override bool FromLong(long backingValue) => backingValue != 0; 20 | 21 | /// 22 | protected override long ToLong(bool value) => value ? 1 : 0; 23 | } 24 | -------------------------------------------------------------------------------- /src/Swan.Net/Smtp/SmtpDefinitions.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Net.Smtp; 2 | 3 | /// 4 | /// Contains useful constants and definitions. 5 | /// 6 | public static class SmtpDefinitions 7 | { 8 | /// 9 | /// The string sequence that delimits the end of the DATA command. 10 | /// 11 | public const string SmtpDataCommandTerminator = "\r\n.\r\n"; 12 | 13 | /// 14 | /// Lists the AUTH methods supported by default. 15 | /// 16 | public static class SmtpAuthMethods 17 | { 18 | /// 19 | /// The plain method. 20 | /// 21 | public const string Plain = "PLAIN"; 22 | 23 | /// 24 | /// The login method. 25 | /// 26 | public const string Login = "LOGIN"; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Swan.Core/Threading/AtomicInteger.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Threading; 2 | 3 | /// 4 | /// Represents an atomically readable or writable integer. 5 | /// 6 | public class AtomicInteger : AtomicTypeBase 7 | { 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | /// if set to true [initial value]. 12 | public AtomicInteger(int initialValue = default) 13 | : base(Convert.ToInt64(initialValue)) 14 | { 15 | // placeholder 16 | } 17 | 18 | /// 19 | protected override int FromLong(long backingValue) => 20 | Convert.ToInt32(backingValue); 21 | 22 | /// 23 | protected override long ToLong(int value) => 24 | Convert.ToInt64(value); 25 | } 26 | -------------------------------------------------------------------------------- /src/Swan.Threading/Threading/IWorkerDelayProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Threading; 2 | 3 | /// 4 | /// An interface for a worker cycle delay provider. 5 | /// 6 | public interface IWorkerDelayProvider 7 | { 8 | /// 9 | /// Suspends execution queues a new cycle for execution. The delay is given in 10 | /// milliseconds. When overridden in a derived class the wait handle will be set 11 | /// whenever an interrupt is received. 12 | /// 13 | /// The remaining delay to wait for in the cycle. 14 | /// Contains a reference to a task with the scheduled period delay. 15 | /// The cancellation token to cancel waiting. 16 | void ExecuteCycleDelay(int wantedDelay, Task delayTask, CancellationToken token); 17 | } 18 | -------------------------------------------------------------------------------- /src/Swan.Logging/Logging/LogLevel.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Logging; 2 | 3 | /// 4 | /// Defines the log levels. 5 | /// 6 | public enum LogLevel 7 | { 8 | /// 9 | /// The none message type 10 | /// 11 | None, 12 | 13 | /// 14 | /// The trace message type 15 | /// 16 | Trace, 17 | 18 | /// 19 | /// The debug message type 20 | /// 21 | Debug, 22 | 23 | /// 24 | /// The information message type 25 | /// 26 | Info, 27 | 28 | /// 29 | /// The warning message type 30 | /// 31 | Warning, 32 | 33 | /// 34 | /// The error message type 35 | /// 36 | Error, 37 | 38 | /// 39 | /// The fatal message type 40 | /// 41 | Fatal, 42 | } 43 | -------------------------------------------------------------------------------- /test/Swan.Test/TestFixtureBase.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test; 2 | 3 | using Mocks; 4 | 5 | public abstract class TestFixtureBase 6 | { 7 | protected Dictionary? NullDict => null; 8 | 9 | protected object? NullObj => null; 10 | 11 | protected string? NullString => null; 12 | 13 | protected List? NullStringList => null; 14 | 15 | protected Type? NullType => null; 16 | 17 | protected Exception? NullException => null; 18 | 19 | protected SampleStruct DefaultStruct => new() 20 | { 21 | Name = nameof(DefaultStruct), 22 | Value = 1, 23 | }; 24 | 25 | protected List DefaultStringList => new() { "A", "B", "C" }; 26 | 27 | protected Dictionary DefaultDictionary => new() 28 | { 29 | { 1, "A" }, 30 | { 2, "B" }, 31 | { 3, "C" }, 32 | { 4, "D" }, 33 | { 5, "E" }, 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /src/Swan.Data/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Swan.Data.Context; 2 | global using Swan.Data.Extensions; 3 | global using Swan.Data.Providers; 4 | global using Swan.Data.Records; 5 | global using Swan.Data.Schema; 6 | global using Swan.Extensions; 7 | global using Swan.Reflection; 8 | global using System; 9 | global using System.Collections; 10 | global using System.Collections.Generic; 11 | global using System.ComponentModel.DataAnnotations; 12 | global using System.ComponentModel.DataAnnotations.Schema; 13 | global using System.Data; 14 | global using System.Data.Common; 15 | global using System.Diagnostics.CodeAnalysis; 16 | global using System.Dynamic; 17 | global using System.Globalization; 18 | global using System.Linq; 19 | global using System.Linq.Expressions; 20 | global using System.Runtime.CompilerServices; 21 | global using System.Text; 22 | global using System.Threading.Tasks; 23 | 24 | [assembly: CLSCompliant(true)] 25 | -------------------------------------------------------------------------------- /src/Swan.Core/Threading/AtomicDouble.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Threading; 2 | 3 | /// 4 | /// Fast, atomic double combining interlocked to write value and volatile to read values. 5 | /// 6 | public sealed class AtomicDouble : AtomicTypeBase 7 | { 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | /// if set to true [initial value]. 12 | public AtomicDouble(double initialValue = default) 13 | : base(BitConverter.DoubleToInt64Bits(initialValue)) 14 | { 15 | // placeholder 16 | } 17 | 18 | /// 19 | protected override double FromLong(long backingValue) => 20 | BitConverter.Int64BitsToDouble(backingValue); 21 | 22 | /// 23 | protected override long ToLong(double value) => 24 | BitConverter.DoubleToInt64Bits(value); 25 | } 26 | -------------------------------------------------------------------------------- /test/Swan.Test/ThreadWorkerBaseTest.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test; 2 | 3 | using Threading; 4 | 5 | [TestFixture] 6 | public class ThreadWorkerBaseTest 7 | { 8 | [Test] 9 | public async Task ThreadWorkerCycle() 10 | { 11 | var worker = new WorkerTest("Test", TimeSpan.FromSeconds(1)); 12 | _ = worker.StartAsync(); 13 | await Task.Delay(1000); 14 | Assert.That(worker.Counter.Value, Is.GreaterThan(0)); 15 | } 16 | } 17 | 18 | internal class WorkerTest : ThreadWorkerBase 19 | { 20 | public AtomicInteger Counter { get; } = new(); 21 | 22 | public WorkerTest(string name, TimeSpan period) 23 | : base(name, ThreadPriority.Highest, period, null) 24 | { 25 | } 26 | 27 | protected override void ExecuteCycleLogic(CancellationToken cancellationToken) 28 | => Counter.Increment(); 29 | 30 | protected override void OnCycleException(Exception ex) => throw new NotImplementedException(); 31 | } 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /test/Swan.Test/Mocks/ContainerMock.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test.Mocks; 2 | 3 | public interface IAnimal 4 | { 5 | string Name { get; } 6 | } 7 | 8 | public class Monkey : IAnimal 9 | { 10 | public string Name => nameof(Monkey); 11 | } 12 | 13 | public class Fish : IAnimal 14 | { 15 | public string Name => nameof(Fish); 16 | 17 | [AttributeMock("This is an Attribute")] 18 | public virtual string GetFeeding() => "Worms"; 19 | } 20 | 21 | [AttributeUsage(AttributeTargets.All)] 22 | public sealed class AttributeMock : Attribute 23 | { 24 | public AttributeMock(string name) 25 | { 26 | Name = name; 27 | } 28 | 29 | public string Name { get; } 30 | } 31 | 32 | public class Clown : Fish 33 | { 34 | [AttributeMock("This is an Attribute")] 35 | public string GetName() => "Nemo"; 36 | } 37 | 38 | public class Shark : Fish 39 | { 40 | public string GetName() => "Lenny"; 41 | 42 | public override string GetFeeding() => "Seals"; 43 | } 44 | -------------------------------------------------------------------------------- /test/Swan.Test/ExtensionsValueTypeTest.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test; 2 | 3 | using Extensions; 4 | 5 | [TestFixture] 6 | public class Clamp 7 | { 8 | [Test] 9 | public void WithValidInt_ClampsValue() 10 | { 11 | Assert.AreEqual(3d, 3d.Clamp(1, 3.1)); 12 | 13 | Assert.AreEqual(1, (-1).Clamp(1, 5d)); 14 | } 15 | 16 | [Test] 17 | public void WithValidDecimal_ClampsValue() 18 | { 19 | Assert.AreEqual(3m, 3.5m.Clamp(1, 3d)); 20 | 21 | Assert.AreEqual(1m, (-1m).Clamp(1, 5m)); 22 | 23 | Assert.AreEqual(-2m, (-6.144m).Clamp(-2, 2d)); 24 | } 25 | } 26 | 27 | [TestFixture] 28 | public class IsBetween 29 | { 30 | [Test] 31 | public void WithValidParams_ReturnsTrue() 32 | { 33 | var aux = 5d.IsBetween(0d, 7m); 34 | Assert.IsTrue(aux); 35 | } 36 | 37 | [Test] 38 | public void WithInvalidParams_ReturnsFalse() 39 | { 40 | var aux = 9L.IsBetween(0U, 7L); 41 | Assert.IsFalse(aux); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/Swan.Test/WaitEventFactoryTest.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test; 2 | 3 | using Threading; 4 | 5 | [TestFixture] 6 | public class WaitEvent 7 | { 8 | [Test] 9 | [TestCase(true, true)] 10 | [TestCase(true, false)] 11 | public void NormalCycle(bool initialValue, bool useSlim) 12 | { 13 | var wait = WaitEventFactory.Create(initialValue, useSlim); 14 | 15 | Assert.IsTrue(wait.IsCompleted); 16 | 17 | wait.Begin(); 18 | 19 | Assert.IsFalse(wait.IsCompleted); 20 | Assert.IsTrue(wait.IsInProgress); 21 | 22 | wait.Complete(); 23 | 24 | Assert.IsTrue(wait.IsCompleted); 25 | Assert.IsFalse(wait.IsInProgress); 26 | } 27 | 28 | [Test] 29 | public void IfDisposed_IsInProgressEqualsFalse() 30 | { 31 | var wait = WaitEventFactory.Create(true); 32 | wait.Begin(); 33 | wait.Dispose(); 34 | wait.Begin(); 35 | 36 | Assert.IsFalse(wait.IsInProgress); 37 | Assert.IsFalse(wait.IsValid); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SWAN: Stuff We All Need (Unosquare's collection of C# extension methods and classes) 2 | 3 | *:star: Please star this project if you find it useful!* 4 | 5 | SWAN stands for Stuff We All Need 6 | 7 | Repeating code and reinventing the wheel is generally considered bad practice. At [Unosquare](https://www.unosquare.com) we are committed to beautiful code and great software. Swan is a collection of classes and extension methods that we (and other good developers) have written and evolved over the years. We found ourselves copying and pasting the same code for every project every time we started them. We decided to kill that cycle once and for all. This is the result of that idea. Our philosophy is that Swan should have no external dependencies, it should be cross-platform, and it should be useful. 8 | 9 | ## 💾 Installation: 10 | 11 | Check the Nugets page for the latest version of SWAN: https://github.com/orgs/unosquare/packages?repo_name=swan 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | registries: 3 | nuget-feed-npm-pkg-github-com-unosquare: 4 | type: nuget-feed 5 | url: https://npm.pkg.github.com/unosquare 6 | token: "${{secrets.NUGET_FEED_NPM_PKG_GITHUB_COM_UNOSQUARE_TOKEN}}" 7 | nuget-feed-nuget-pkg-github-com-unosquare-index-json: 8 | type: nuget-feed 9 | url: https://nuget.pkg.github.com/unosquare/index.json 10 | username: "${{secrets.NUGET_FEED_NUGET_PKG_GITHUB_COM_UNOSQUARE_INDEX_JSON_USERNAME}}" 11 | password: "${{secrets.NUGET_FEED_NUGET_PKG_GITHUB_COM_UNOSQUARE_INDEX_JSON_PASSWORD}}" 12 | 13 | updates: 14 | - package-ecosystem: nuget 15 | directory: "/" 16 | schedule: 17 | interval: daily 18 | time: "11:00" 19 | open-pull-requests-limit: 10 20 | registries: 21 | - nuget-feed-npm-pkg-github-com-unosquare 22 | - nuget-feed-nuget-pkg-github-com-unosquare-index-json 23 | - package-ecosystem: github-actions 24 | directory: "/" 25 | schedule: 26 | interval: daily 27 | time: "11:00" 28 | open-pull-requests-limit: 10 29 | -------------------------------------------------------------------------------- /src/Swan.Core/Collections/CollectionEnumerator.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Collections; 2 | 3 | internal struct CollectionEnumerator : IDictionaryEnumerator, IEnumerator 4 | { 5 | private int _currentIndex = -1; 6 | 7 | public CollectionEnumerator(CollectionProxy proxy) => Proxy = proxy; 8 | 9 | public DictionaryEntry Entry => _currentIndex >= 0 ? new(Key, Value) : default; 10 | 11 | public object Key => _currentIndex; 12 | 13 | public object? Value => _currentIndex >= 0 ? Proxy[_currentIndex] : default; 14 | 15 | public object? Current => _currentIndex >= 0 ? Entry : default; 16 | 17 | private CollectionProxy Proxy { get; } 18 | 19 | public bool MoveNext() 20 | { 21 | var elementCount = Proxy.Count; 22 | _currentIndex++; 23 | if (_currentIndex < elementCount) 24 | return true; 25 | 26 | _currentIndex = elementCount - 1; 27 | return false; 28 | } 29 | 30 | public void Reset() => _currentIndex = -1; 31 | 32 | void IDisposable.Dispose() 33 | { 34 | // placeholder 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/Swan.Test/FileLoggerTest.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test; 2 | 3 | using Logging; 4 | 5 | [TestFixture] 6 | public class FileLoggerTest 7 | { 8 | [Test] 9 | public async Task WithDefaultValues_FileExist() 10 | { 11 | if (!OperatingSystem.IsWindows()) 12 | Assert.Ignore("Ignored"); 13 | 14 | var instance = new FileLogger(); 15 | Logger.RegisterLogger(instance); 16 | "Test".Info(); 17 | await Task.Delay(1); 18 | Logger.UnregisterLogger(instance); 19 | 20 | Assert.IsTrue(File.Exists(instance.FilePath)); 21 | } 22 | 23 | [Test] 24 | public async Task WithDefaultValues_FileIsNotEmpty() 25 | { 26 | if (!OperatingSystem.IsWindows()) 27 | Assert.Ignore("Ignored"); 28 | 29 | var instance = new FileLogger(); 30 | Logger.RegisterLogger(instance); 31 | "Test".Info(); 32 | await Task.Delay(1); 33 | Logger.UnregisterLogger(instance); 34 | 35 | var logContent = await File.ReadAllTextAsync(instance.FilePath); 36 | Assert.IsNotEmpty(logContent); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/Swan.Test/Mocks/AppWorkerMock.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test.Mocks; 2 | 3 | using Threading; 4 | 5 | public class AppWorkerMock : TimerWorkerBase 6 | { 7 | public AppWorkerMock() 8 | : base(nameof(AppWorkerMock), TimeSpan.FromMilliseconds(100)) 9 | { 10 | } 11 | 12 | public Exception Exception { get; private set; } 13 | public bool ExitBecauseCancellation { get; private set; } = true; 14 | public int Count { get; private set; } 15 | 16 | public Action? OnExit { get; set; } 17 | 18 | public override Task StopAsync() 19 | { 20 | OnExit?.Invoke(); 21 | 22 | return base.StopAsync(); 23 | } 24 | 25 | protected override void OnCycleException(Exception ex) => Exception = ex; 26 | 27 | protected override void ExecuteCycleLogic(CancellationToken ct) 28 | { 29 | if (++Count != 6) return; 30 | 31 | ExitBecauseCancellation = false; 32 | throw new InvalidOperationException("Expected exception"); 33 | } 34 | 35 | protected override void OnDisposing() 36 | { 37 | // do nothing 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2020 Unosquare, Mario A. Di Vece and Geovanni Perez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Swan.Samples/Swan.Samples.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Some usage samples of the SWAN library 5 | Copyright (c) 2016-2022 - Unosquare 6 | Unosquare SWAN Samples 7 | net7.0 8 | Swan.Samples 9 | Exe 10 | Unosquare.Swan.Samples 11 | latest 12 | enable 13 | enable 14 | ..\..\StyleCop.Analyzers.ruleset 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/Swan.Test/TerminalTest.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test; 2 | 3 | using Platform; 4 | using System.Text; 5 | 6 | [TestFixture] 7 | public class IsConsolePresent 8 | { 9 | [Test] 10 | public void ConsolePresent_ReturnsTrue() 11 | { 12 | if (OperatingSystem.IsWindows()) 13 | Assert.Ignore("Failing test on Windows"); 14 | 15 | Assert.IsTrue(Terminal.IsConsolePresent); 16 | } 17 | } 18 | 19 | [TestFixture] 20 | public class AvailableWriters 21 | { 22 | [Test] 23 | public void Writers_ReturnsNotEqualWriters() 24 | { 25 | if (OperatingSystem.IsWindows()) 26 | Assert.Ignore("Windows doesn't provide writers"); 27 | 28 | var writers = Terminal.AvailableWriters; 29 | 30 | Assert.AreNotEqual(writers, TerminalWriterFlags.None, "Check for at least one available writer"); 31 | } 32 | } 33 | 34 | [TestFixture] 35 | public class OutputEncoding 36 | { 37 | [Test] 38 | public void DefaultEncoding_ReturnsEqualEncoding() 39 | { 40 | var defaultEncoding = Terminal.OutputEncoding; 41 | 42 | Assert.IsNotNull(defaultEncoding); 43 | 44 | Terminal.OutputEncoding = Encoding.UTF8; 45 | 46 | Assert.AreEqual(Terminal.OutputEncoding, Encoding.UTF8, "Change to UTF8 encoding"); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Swan.Core/Threading/AtomicEnum.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Threading; 2 | 3 | /// 4 | /// Defines an atomic generic Enum. 5 | /// 6 | /// The type of enum. 7 | public sealed class AtomicEnum 8 | where T : struct, IConvertible 9 | { 10 | private long _backingValue; 11 | 12 | /// 13 | /// Initializes a new instance of the class. 14 | /// 15 | /// The initial value. 16 | /// T must be an enumerated type. 17 | public AtomicEnum(T initialValue) 18 | { 19 | if (!Enum.IsDefined(typeof(T), initialValue)) 20 | throw new ArgumentException("T must be an enumerated type"); 21 | 22 | Value = initialValue; 23 | } 24 | 25 | /// 26 | /// Gets or sets the value. 27 | /// 28 | public T Value 29 | { 30 | get => (T)Enum.ToObject(typeof(T), BackingValue); 31 | set => BackingValue = Convert.ToInt64(value, CultureInfo.InvariantCulture); 32 | } 33 | 34 | private long BackingValue 35 | { 36 | get => Interlocked.Read(ref _backingValue); 37 | set => Interlocked.Exchange(ref _backingValue, value); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Swan.Data/Data/Schema/DbColumnSchema.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Data.Schema; 2 | 3 | internal sealed record DbColumnSchema : IDbColumnSchema 4 | { 5 | public DbColumnSchema() 6 | { 7 | // placeholder 8 | } 9 | 10 | public string ColumnName { get; set; } = string.Empty; 11 | 12 | public Type DataType { get; set; } = typeof(string); 13 | 14 | public string DataTypeName { get; set; } = string.Empty; 15 | 16 | public string ProviderType { get; set; } = string.Empty; 17 | 18 | public int ColumnOrdinal { get; set; } 19 | 20 | public int ColumnSize { get; set; } 21 | 22 | public int NumericPrecision { get; set; } 23 | 24 | public int NumericScale { get; set; } 25 | 26 | public bool IsLong { get; set; } 27 | 28 | public bool AllowDBNull { get; set; } 29 | 30 | public bool IsReadOnly { get; set; } 31 | 32 | public bool IsUnique { get; set; } 33 | 34 | public bool IsKey { get; set; } 35 | 36 | public bool IsAutoIncrement { get; set; } 37 | 38 | public string BaseCatalogName { get; set; } = string.Empty; 39 | 40 | public string BaseSchemaName { get; set; } = string.Empty; 41 | 42 | public string BaseTableName { get; set; } = string.Empty; 43 | 44 | public string BaseColumnName { get; set; } = string.Empty; 45 | 46 | object ICloneable.Clone() => this with { }; 47 | } 48 | -------------------------------------------------------------------------------- /src/Swan.Data.SqlBulkOps/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Data.SqlBulkOps; 2 | /// 3 | /// Defines constants for bulk operations. 4 | /// 5 | public static class Constants 6 | { 7 | /// 8 | /// The default notification frequency in number of rows copied. 9 | /// 10 | public const int DefaultNotifyAfter = 100; 11 | 12 | /// 13 | /// The minimum notification frequency in number of rows copied. 14 | /// 15 | public const int MinNotifyAfter = 10; 16 | 17 | /// 18 | /// The maximum notification frequency in number of rows copied. 19 | /// 20 | public const int MaxNotifyAfter = 1000; 21 | 22 | /// 23 | /// The default batch size in number of rows copied. 24 | /// 25 | public const int DefaultBatchSize = 1000; 26 | 27 | /// 28 | /// The minimum batch size in number of rows copied. 29 | /// 30 | public const int MinBatchSize = 10; 31 | 32 | /// 33 | /// The maximum batch size in number of rows copied. 34 | /// 35 | public const int MaxBatchSize = 10000; 36 | 37 | /// 38 | /// Represents an indefinite wait time for the bulk copy operation to complete. 39 | /// 40 | public const int InfiniteTimeoutSeconds = 0; 41 | } 42 | -------------------------------------------------------------------------------- /src/Swan.Core/Reflection/CollectionKind.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Reflection; 2 | 3 | /// 4 | /// Enumerates general collection kinds. 5 | /// 6 | public enum CollectionKind 7 | { 8 | /// 9 | /// Invalid collection kind. 10 | /// 11 | None, 12 | 13 | /// 14 | /// Collection implements . 15 | /// 16 | GenericDictionary, 17 | 18 | /// 19 | /// Collection implements . 20 | /// 21 | Dictionary, 22 | 23 | /// 24 | /// Collection implements . 25 | /// 26 | GenericList, 27 | 28 | /// 29 | /// Collection implements . 30 | /// 31 | List, 32 | 33 | /// 34 | /// Collection implements . 35 | /// 36 | GenericCollection, 37 | 38 | /// 39 | /// Collection implements . 40 | /// 41 | Collection, 42 | 43 | /// 44 | /// Collection implements . 45 | /// 46 | GenericEnumerable, 47 | 48 | /// 49 | /// Collection implements . 50 | /// 51 | Enumerable, 52 | } 53 | -------------------------------------------------------------------------------- /src/Swan.Core/Reflection/TypeManager.Definitions.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Reflection; 2 | 3 | public static partial class TypeManager 4 | { 5 | /// 6 | /// Provides a collection of primitive, numeric types. 7 | /// 8 | public static IReadOnlyList NumericTypes { get; } = new[] 9 | { 10 | typeof(byte), 11 | typeof(sbyte), 12 | typeof(decimal), 13 | typeof(double), 14 | typeof(float), 15 | typeof(int), 16 | typeof(uint), 17 | typeof(long), 18 | typeof(ulong), 19 | typeof(short), 20 | typeof(ushort), 21 | }; 22 | 23 | /// 24 | /// Provides a collection of basic value types including numeric types, 25 | /// string, guid, timespan, and datetime. 26 | /// 27 | public static IReadOnlyList BasicValueTypes { get; } = new[] 28 | { 29 | typeof(int), 30 | typeof(bool), 31 | typeof(string), 32 | typeof(DateTime), 33 | typeof(double), 34 | typeof(decimal), 35 | typeof(Guid), 36 | typeof(long), 37 | typeof(TimeSpan), 38 | typeof(uint), 39 | typeof(float), 40 | typeof(byte), 41 | typeof(short), 42 | typeof(sbyte), 43 | typeof(ushort), 44 | typeof(ulong), 45 | typeof(char), 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /test/Swan.Test/JsonTest.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test; 2 | 3 | using Mocks; 4 | 5 | public abstract class JsonTest : TestFixtureBase 6 | { 7 | protected const string ArrayStruct = "[{\"Value\":1,\"Name\":\"A\"},{\"Value\":2,\"Name\":\"B\"}]"; 8 | 9 | protected static readonly AdvJson AdvObj = new() 10 | { 11 | StringData = "string,\r\ndata\\", 12 | IntData = 1, 13 | NegativeInt = -1, 14 | DecimalData = 10.33M, 15 | BoolData = true, 16 | InnerChild = BasicJson.GetDefault(), 17 | }; 18 | 19 | protected static string BasicStr => $"{{{BasicJson.GetControlValue()}}}"; 20 | 21 | protected string AdvStr => 22 | $"{{\"InnerChild\":{BasicStr},{BasicJson.GetControlValue()}}}"; 23 | 24 | protected int[] NumericArray => new[] { 1, 2, 3 }; 25 | 26 | protected string NumericAStr => "[1,2,3]"; 27 | 28 | protected BasicArrayJson BasicAObj => new() 29 | { 30 | Id = 1, 31 | Properties = new[] { "One", "Two", "Babu" }, 32 | }; 33 | 34 | protected AdvArrayJson AdvAObj => new() 35 | { 36 | Id = 1, 37 | Properties = new[] { BasicJson.GetDefault(), BasicJson.GetDefault() }, 38 | }; 39 | 40 | protected string BasicAObjStr => "{\"Id\":1,\"Properties\":[\"One\",\"Two\",\"Babu\"]}"; 41 | 42 | protected string AdvAStr => $"{{\"Id\":1,\"Properties\":[{BasicStr},{BasicStr}]}}"; 43 | } 44 | -------------------------------------------------------------------------------- /src/Swan.Artifacts/Utilities/ProcessResult.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Utilities; 2 | 3 | /// 4 | /// Represents the text of the standard output and standard error 5 | /// of a process, including its exit code. 6 | /// 7 | public class ProcessResult 8 | { 9 | /// 10 | /// Initializes a new instance of the class. 11 | /// 12 | /// The exit code. 13 | /// The standard output. 14 | /// The standard error. 15 | public ProcessResult(int exitCode, string standardOutput, string standardError) 16 | { 17 | ExitCode = exitCode; 18 | StandardOutput = standardOutput; 19 | StandardError = standardError; 20 | } 21 | 22 | /// 23 | /// Gets the exit code. 24 | /// 25 | /// 26 | /// The exit code. 27 | /// 28 | public int ExitCode { get; } 29 | 30 | /// 31 | /// Gets the text of the standard output. 32 | /// 33 | /// 34 | /// The standard output. 35 | /// 36 | public string StandardOutput { get; } 37 | 38 | /// 39 | /// Gets the text of the standard error. 40 | /// 41 | /// 42 | /// The standard error. 43 | /// 44 | public string StandardError { get; } 45 | } 46 | -------------------------------------------------------------------------------- /src/Swan.Net/Swan.Net.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Swan.Net 5 | Swan 6 | net6.0 7 | latest 8 | enable 9 | true 10 | true 11 | AllEnabledByDefault 12 | enable 13 | 14 | 15 | 16 | This package provides network utilities for the SWAN library. 17 | Copyright (c) 2016-2022 - Unosquare 18 | Unosquare SWAN 19 | true 20 | 5.0.1 21 | Unosquare 22 | https://github.com/unosquare/swan/raw/master/swan-logo-32.png 23 | https://github.com/unosquare/swan 24 | https://raw.githubusercontent.com/unosquare/swan/master/LICENSE 25 | best-practices netcore network objectmapper json-serialization 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /test/Swan.Test/JsonFormatterDynamicTest.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test; 2 | 3 | using NUnit.Framework; 4 | using Formatters; 5 | using Mocks; 6 | using System.Dynamic; 7 | using System.Text.Json; 8 | 9 | [TestFixture] 10 | public class JsonFormatterDynamicTest 11 | { 12 | [Test] 13 | public void WithJsonTextNullOrWitheSpace_ReturnsNull() 14 | { 15 | var jsonText = string.Empty; 16 | var jsonObject = jsonText.JsonDeserialize(); 17 | 18 | Assert.IsNull(jsonObject); 19 | } 20 | 21 | [Test] 22 | public void WithJsonText_ReturnsObject() 23 | { 24 | var jsonText = "{\r\n \"Name\": \"Merlina\"\r\n}"; 25 | var jsonObject = jsonText.JsonDeserialize(); 26 | 27 | Assert.AreEqual("Merlina", jsonObject?.Name); 28 | } 29 | 30 | [Test] 31 | public async Task WithNullStream_ReturnsNull() 32 | { 33 | var jsonStream = null as Stream; 34 | var jsonObject = await jsonStream.JsonDeserializeAsync(); 35 | 36 | Assert.IsNull(jsonObject); 37 | } 38 | 39 | [Test] 40 | public async Task WithUTF8JsonStream_ReturnsDynamicObject() 41 | { 42 | var dog = new Dog { Name = "Merlina" }; 43 | var jsonUtf8Bytes = JsonSerializer.SerializeToUtf8Bytes(dog); 44 | 45 | var stream = new MemoryStream(jsonUtf8Bytes); 46 | 47 | var deserializedDog = await stream.JsonDeserializeAsync(); 48 | 49 | Assert.AreEqual(typeof(ExpandoObject), deserializedDog.GetType()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Swan.Logging/Platform/Terminal.Graphics.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Platform; 2 | 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | /// 6 | /// A console terminal helper to create nicer output and receive input from the user 7 | /// This class is thread-safe :). 8 | /// 9 | [ExcludeFromCodeCoverage] 10 | public static partial class Terminal 11 | { 12 | /// 13 | /// Represents a Table to print in console. 14 | /// 15 | private static class Table 16 | { 17 | public static void Vertical() => Write('\u2502', Settings.BorderColor); 18 | 19 | public static void RightTee() => Write('\u2524', Settings.BorderColor); 20 | 21 | public static void TopRight() => Write('\u2510', Settings.BorderColor); 22 | 23 | public static void BottomLeft() => Write('\u2514', Settings.BorderColor); 24 | 25 | public static void BottomTee() => Write('\u2534', Settings.BorderColor); 26 | 27 | public static void TopTee() => Write('\u252c', Settings.BorderColor); 28 | 29 | public static void LeftTee() => Write('\u251c', Settings.BorderColor); 30 | 31 | public static void Horizontal(int length) => Write(new string('\u2500', length), Settings.BorderColor); 32 | 33 | public static void Tee() => Write('\u253c', Settings.BorderColor); 34 | 35 | public static void BottomRight() => Write('\u2518', Settings.BorderColor); 36 | 37 | public static void TopLeft() => Write('\u250C', Settings.BorderColor); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/Swan.Test/PaginatorTest.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test; 2 | 3 | using Gizmos; 4 | 5 | [TestFixture] 6 | public class PaginatorTest 7 | { 8 | [Test] 9 | public void WithValidData_ReturnPageSize() 10 | { 11 | var stu = new Paginator(100, 10); 12 | Assert.AreEqual(10, stu.PageSize); 13 | } 14 | 15 | [Test] 16 | public void WithValidData_ReturnPageCount() 17 | { 18 | var stu = new Paginator(100, 10); 19 | Assert.AreEqual(10, stu.PageCount); 20 | } 21 | 22 | [Test] 23 | public void WithValidData_ReturnTotalCount() 24 | { 25 | var stu = new Paginator(100, 10); 26 | Assert.AreEqual(100, stu.TotalCount); 27 | } 28 | 29 | [Test] 30 | public void WithValidDataAtIndexNine_ReturnGetItemCount() 31 | { 32 | var stu = new Paginator(99, 10); 33 | Assert.AreEqual(9, stu.GetItemCount(9)); 34 | } 35 | 36 | [Test] 37 | public void WithValidDataAtIndexNine_ReturnGetFirstItemIndex() 38 | { 39 | var stu = new Paginator(99, 10); 40 | Assert.AreEqual(90, stu.GetFirstItemIndex(9)); 41 | } 42 | 43 | [Test] 44 | public void WithValidDataAtIndexNine_ReturnGetLastItemIndex() 45 | { 46 | var stu = new Paginator(99, 10); 47 | Assert.AreEqual(98, stu.GetLastItemIndex(9)); 48 | } 49 | 50 | [Test] 51 | public void WithTotalCountZero_ReturnPageCountEqualsZero() 52 | { 53 | var stu = new Paginator(0, 10); 54 | Assert.AreEqual(0, stu.PageCount); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/Swan.Test/Swan.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Unit Testing project 5 | Copyright (c) 2016-2022 - Unosquare 6 | Unosquare SWAN Test 7 | net7.0 8 | Unosquare.Swan.Test 9 | latest 10 | enable 11 | enable 12 | true 13 | true 14 | AllEnabledByDefault 15 | ..\..\StyleCop.Analyzers.ruleset 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | all 29 | runtime; build; native; contentfiles; analyzers; buildtransitive 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/Swan.Logging/Platform/Terminal.Settings.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Platform; 2 | 3 | using System; 4 | 5 | /// 6 | /// A console terminal helper to create nicer output and receive input from the user 7 | /// This class is thread-safe :). 8 | /// 9 | public static partial class Terminal 10 | { 11 | /// 12 | /// Terminal global settings. 13 | /// 14 | public static class Settings 15 | { 16 | /// 17 | /// Gets or sets the default output color. 18 | /// 19 | /// 20 | /// The default color. 21 | /// 22 | public static ConsoleColor DefaultColor { get; set; } = Console.ForegroundColor; 23 | 24 | /// 25 | /// Gets the color of the border. 26 | /// 27 | /// 28 | /// The color of the border. 29 | /// 30 | public static ConsoleColor BorderColor { get; } = ConsoleColor.DarkGreen; 31 | 32 | /// 33 | /// Gets or sets the user input prefix. 34 | /// 35 | /// 36 | /// The user input prefix. 37 | /// 38 | public static string UserInputPrefix { get; set; } = "USR"; 39 | 40 | /// 41 | /// Gets or sets the user option text. 42 | /// 43 | /// 44 | /// The user option text. 45 | /// 46 | public static string UserOptionText { get; set; } = " Option: "; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Swan.Logging/Logging/DebugLogger.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Logging; 2 | 3 | /// 4 | /// Represents a logger target. This target will write to the 5 | /// Debug console using System.Diagnostics.Debug. 6 | /// 7 | /// 8 | public class DebugLogger : TextLogger, ILogger 9 | { 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | protected DebugLogger() 14 | { 15 | // Empty 16 | } 17 | 18 | /// 19 | /// Gets the current instance of DebugLogger. 20 | /// 21 | /// 22 | /// The instance. 23 | /// 24 | public static DebugLogger Instance { get; } = new(); 25 | 26 | /// 27 | /// Gets a value indicating whether a debugger is attached. 28 | /// 29 | /// 30 | /// true if this instance is debugger attached; otherwise, false. 31 | /// 32 | public static bool IsDebuggerAttached => System.Diagnostics.Debugger.IsAttached; 33 | 34 | /// 35 | public LogLevel LogLevel { get; set; } = IsDebuggerAttached ? LogLevel.Trace : LogLevel.None; 36 | 37 | /// 38 | public void Log(LogMessageReceivedEventArgs logEvent) 39 | { 40 | var (outputMessage, _) = GetOutputAndColor(logEvent); 41 | 42 | System.Diagnostics.Debug.Write(outputMessage); 43 | } 44 | 45 | /// 46 | public void Dispose() 47 | { 48 | // do nothing 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Swan.Core/Formatters/CsvReader.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Formatters; 2 | 3 | using System.IO; 4 | 5 | /// 6 | /// Represents a text reader, typically a comma-separated set of values 7 | /// that can be configured to read an parse tabular data with flexible 8 | /// encoding, field separators and escape characters. 9 | /// 10 | public class CsvReader : CsvReaderBase> 11 | { 12 | /// 13 | /// Creates a new instance of the class. 14 | /// 15 | /// The stream to read. 16 | /// The character encoding to use. 17 | /// The field separator character. 18 | /// The escape character. 19 | /// true to leave the stream open after the stream reader object is disposed; otherwise, false. 20 | /// True to trim field values as they are read and parsed. 21 | public CsvReader(Stream stream, 22 | Encoding? encoding = default, 23 | char separatorChar = Csv.DefaultSeparatorChar, 24 | char escapeChar = Csv.DefaultEscapeChar, 25 | bool leaveOpen = default, 26 | bool trimsValues = true) 27 | : base(stream, encoding, separatorChar, escapeChar, leaveOpen, trimsValues) 28 | { 29 | } 30 | 31 | /// 32 | public override IReadOnlyList Current => 33 | Values ?? throw new InvalidOperationException("Reader is not in a valid state."); 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/ci-all.yml: -------------------------------------------------------------------------------- 1 | # This GitHub Action bumps up the tag in the csproj file depending on the commit's comment: 2 | # SemVer format is: MAJOR.MINOR.PATCH-PRERELEASE 3 | # If the commit's comment contains the string [MAJOR], [MINOR] or [PATCH] it bumps it accordingly. 4 | # If no matching bump commment is found, the PRERELEASE component gets bumped. 5 | # Finally, it builds the project in .net 5 and .net 6, and publishes it to the local GitHub package registry. 6 | # by @geoperez and @mariodivece 7 | name: Pack and Publish SWAN Libraries 8 | on: 9 | push: 10 | branches: [ master ] 11 | 12 | jobs: 13 | swan-core: 14 | uses: unosquare/swan/.github/workflows/lib-ci.yml@master 15 | with: 16 | source-path: 'src/' 17 | project-name: 'Swan.Core' 18 | secrets: 19 | github-token: ${{ secrets.MAD_PACKAGE_MGMNT }} 20 | swan-logging: 21 | needs: swan-core 22 | uses: unosquare/swan/.github/workflows/lib-ci.yml@master 23 | with: 24 | source-path: 'src/' 25 | project-name: 'Swan.Logging' 26 | secrets: 27 | github-token: ${{ secrets.MAD_PACKAGE_MGMNT }} 28 | swan-data: 29 | needs: swan-logging 30 | uses: unosquare/swan/.github/workflows/lib-ci.yml@master 31 | with: 32 | source-path: 'src/' 33 | project-name: 'Swan.Data' 34 | secrets: 35 | github-token: ${{ secrets.MAD_PACKAGE_MGMNT }} 36 | swan-data-sqlbulkops: 37 | needs: swan-data 38 | uses: unosquare/swan/.github/workflows/lib-ci.yml@master 39 | with: 40 | source-path: 'src/' 41 | project-name: 'Swan.Data.SqlBulkOps' 42 | secrets: 43 | github-token: ${{ secrets.MAD_PACKAGE_MGMNT }} 44 | -------------------------------------------------------------------------------- /test/Swan.Test/AppWorkerBaseTest.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test; 2 | 3 | using Mocks; 4 | using Threading; 5 | 6 | [TestFixture] 7 | public class AppWorkerBaseTest 8 | { 9 | [Test] 10 | public async Task CanStartAndStopTest() 11 | { 12 | var mock = new AppWorkerMock(); 13 | var exit = false; 14 | mock.OnExit = () => exit = true; 15 | Assert.AreEqual(WorkerState.Created, mock.WorkerState); 16 | await mock.StartAsync(); 17 | Assert.AreEqual(WorkerState.Waiting, mock.WorkerState); 18 | await mock.StopAsync(); 19 | Assert.AreEqual(WorkerState.Stopped, mock.WorkerState); 20 | 21 | Assert.IsTrue(mock.ExitBecauseCancellation, "Exit because cancellation"); 22 | Assert.IsTrue(exit, "Exit event was fired"); 23 | } 24 | 25 | [Test] 26 | public async Task WorkingTest() 27 | { 28 | var mock = new AppWorkerMock(); 29 | await mock.StartAsync(); 30 | 31 | // Mock increase count by one every 100 ms, wait a little bit 32 | await Task.Delay(TimeSpan.FromSeconds(1)); 33 | Assert.GreaterOrEqual(mock.Count, 5); 34 | } 35 | 36 | [Test] 37 | public async Task AppWorkerExceptionTest() 38 | { 39 | var mock = new AppWorkerMock(); 40 | await mock.StartAsync(); 41 | 42 | // Mock increase count by one every 100 ms, wait a little bit 43 | await Task.Delay(TimeSpan.FromSeconds(2)); 44 | 45 | Assert.AreEqual(WorkerState.Waiting, mock.WorkerState); 46 | Assert.IsFalse(mock.ExitBecauseCancellation, "The AppWorker doesn't exit because cancellation"); 47 | Assert.IsNotNull(mock.Exception, "The AppWorker had an exception"); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/Swan.Test/JsonFormatterTest.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test; 2 | 3 | using NUnit.Framework; 4 | using Formatters; 5 | using Mocks; 6 | using System.Text.Json; 7 | 8 | [TestFixture] 9 | public class JsonFormatterTest 10 | { 11 | [Test] 12 | public void WithJson_ReturnsDogObject() 13 | { 14 | var deserialized = "{\r\n \"Name\": \"Merlina\"\r\n}".JsonDeserialize(typeof(Dog)) as Dog; 15 | Assert.AreEqual("Merlina", deserialized?.Name); 16 | } 17 | 18 | [Test] 19 | public void WithNullType_ThrowsException() 20 | { 21 | Stream stream = null; 22 | Assert.Throws(() => "{\r\n \"Name\": \"Merlina\"\r\n}".JsonDeserialize(null)); 23 | Assert.ThrowsAsync(() => stream.JsonDeserializeAsync(null)); 24 | } 25 | 26 | [Test] 27 | public async Task WithUTF8JsonStream_ReturnsObject() 28 | { 29 | var dog = new Dog { Name = "Merlina" }; 30 | byte[] jsonUtf8Bytes = JsonSerializer.SerializeToUtf8Bytes(dog); 31 | 32 | var stream = new MemoryStream(jsonUtf8Bytes); 33 | 34 | var deserializedDog = await stream.JsonDeserializeAsync(typeof(Dog)) as Dog; 35 | 36 | Assert.AreEqual(dog.Name, deserializedDog?.Name); 37 | } 38 | 39 | [Test] 40 | public async Task WithUTF8JsonStream_ReturnsDinamicObject() 41 | { 42 | var dog = new Dog { Name = "Merlina" }; 43 | byte[] jsonUtf8Bytes = JsonSerializer.SerializeToUtf8Bytes(dog); 44 | 45 | var stream = new MemoryStream(jsonUtf8Bytes); 46 | 47 | var deserializedDog = await stream.JsonDeserializeAsync(); 48 | 49 | Assert.AreEqual(dog.Name, deserializedDog?.Name); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Swan.Core/Reflection/ICollectionInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Reflection; 2 | 3 | /// 4 | /// Provides type metadata on a collection type 5 | /// by taking the most capable collection interface available on the 6 | /// 7 | public interface ICollectionInfo 8 | { 9 | /// 10 | /// Gets the type proxy that generated and owns this collection type proxy. 11 | /// 12 | ITypeInfo SourceType { get; } 13 | 14 | /// 15 | /// Gets the underlying collection kind. 16 | /// 17 | CollectionKind CollectionKind { get; } 18 | 19 | /// 20 | /// Gets the underlying constructed interface type 21 | /// that this collection implements. 22 | /// 23 | ITypeInfo CollectionType { get; } 24 | 25 | /// 26 | /// Gets the type proxy for keys in dictionaries. 27 | /// For non-dictionaries, returns the proxy for . 28 | /// 29 | ITypeInfo KeysType { get; } 30 | 31 | /// 32 | /// Gets the type proxy for values in the collection. 33 | /// For dictionaries it gets the value type in the key-value pairs. 34 | /// For other collection types, it returns the item type. 35 | /// 36 | ITypeInfo ValuesType { get; } 37 | 38 | /// 39 | /// Gets a value indicating that the collection type is a dictionary. 40 | /// This specifies that the might not be . 41 | /// 42 | bool IsDictionary { get; } 43 | 44 | /// 45 | /// Gets a value indicating that the collection type is an array. 46 | /// 47 | bool IsArray { get; } 48 | } 49 | -------------------------------------------------------------------------------- /test/Swan.Test/TableContextTest.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test; 2 | 3 | using NUnit.Framework; 4 | using Mocks; 5 | 6 | [TestFixture] 7 | public class TableContextTest 8 | { 9 | [Test] 10 | public void BuildCommandMethodsReturnInsertUpdateDeleteSelectQuerySyntax() 11 | { 12 | var conn = new SqliteConnection("Data Source=:memory:"); 13 | var builder = conn.EnsureConnected().TableBuilder("Projects"); 14 | builder.ExecuteTableCommand(); 15 | 16 | var table = conn.Table("Projects"); 17 | var tran = conn.BeginTransaction(); 18 | 19 | var insertCommand = table.BuildInsertCommand(tran); 20 | var updateCommand = table.BuildUpdateCommand(tran); 21 | var deleteCommand = table.BuildDeleteCommand(tran); 22 | var selectCommand = table.BuildSelectCommand(tran); 23 | 24 | Assert.AreEqual("INSERT INTO [Projects] ( [Name], [ProjectType], [CompanyId], [IsActive], [StartDate], [EndDate], [ProjectScope] ) VALUES ( $Name, $ProjectType, $CompanyId, $IsActive, $StartDate, $EndDate, $ProjectScope )", 25 | insertCommand.CommandText); 26 | Assert.AreEqual("UPDATE [Projects] SET [Name] = $Name, [ProjectType] = $ProjectType, [CompanyId] = $CompanyId, [IsActive] = $IsActive, [StartDate] = $StartDate, [EndDate] = $EndDate, [ProjectScope] = $ProjectScope WHERE [ProjectId] = $ProjectId", 27 | updateCommand.CommandText); 28 | Assert.AreEqual("DELETE FROM [Projects] WHERE [ProjectId] = $ProjectId", 29 | deleteCommand.CommandText); 30 | Assert.AreEqual("SELECT [ProjectId], [Name], [ProjectType], [CompanyId], [IsActive], [StartDate], [EndDate], [ProjectScope] FROM [Projects] WHERE [ProjectId] = $ProjectId", 31 | selectCommand.CommandText); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Swan.Net/Smtp/SmtpSender.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Net.Smtp; 2 | 3 | using Logging; 4 | using System; 5 | using System.Linq; 6 | using System.Net.Mail; 7 | 8 | /// 9 | /// Use this class to store the sender session data. 10 | /// 11 | internal class SmtpSender 12 | { 13 | private readonly string? _sessionId; 14 | private string? _requestText; 15 | 16 | public SmtpSender(string? sessionId) 17 | { 18 | _sessionId = sessionId; 19 | } 20 | 21 | public string? RequestText 22 | { 23 | get => _requestText; 24 | set 25 | { 26 | _requestText = value; 27 | $" TX {_requestText}".Trace(typeof(SmtpClient), _sessionId); 28 | } 29 | } 30 | 31 | public string? ReplyText { get; set; } 32 | 33 | public bool IsReplyOk => ReplyText?.StartsWith("250 ", StringComparison.OrdinalIgnoreCase) == true; 34 | 35 | public void ValidateReply() 36 | { 37 | if (ReplyText == null) 38 | throw new SmtpException("There was no response from the server"); 39 | 40 | try 41 | { 42 | var response = SmtpServerReply.Parse(ReplyText); 43 | $" RX {ReplyText} - {response.IsPositive}".Trace(typeof(SmtpClient), _sessionId); 44 | 45 | if (response.IsPositive) return; 46 | 47 | var responseContent = response.Content.Any() 48 | ? string.Join(";", response.Content.ToArray()) 49 | : string.Empty; 50 | 51 | throw new SmtpException((SmtpStatusCode)response.ReplyCode, responseContent); 52 | } 53 | catch (Exception ex) 54 | { 55 | if (!(ex is SmtpException)) 56 | throw new SmtpException($"Could not parse server response: {ReplyText}"); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/Swan.Test/CsvDynamicReaderTest.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test; 2 | 3 | using NUnit.Framework; 4 | using Formatters; 5 | using System.Text; 6 | 7 | [TestFixture] 8 | public class CsvDynamicReaderTest 9 | { 10 | private const string Data = @"Company,OpenPositions,MainTechnology,Revenue 11 | Co,2,""C#, MySQL, JavaScript, HTML5 and CSS3"","" $1,359,885 "" 12 | Ca,2,""C#, MySQL, JavaScript, HTML5 and CSS3"","" $1,359,885 """; 13 | 14 | private static readonly MemoryStream stream = new(Encoding.ASCII.GetBytes(Data)); 15 | private readonly CsvDynamicReader dynamicReader = new(stream); 16 | private readonly Dictionary dict = new() 17 | { 18 | { "Company", "Company" }, 19 | }; 20 | 21 | [Test] 22 | public void WithNullValues_ThrowsException() 23 | { 24 | Assert.Throws(() => dynamicReader.AddMappings(null)); 25 | Assert.Throws(() => dynamicReader.AddMapping(null, null)); 26 | Assert.Throws(() => dynamicReader.AddMapping("Company", null)); 27 | Assert.Throws(() => dynamicReader.AddMapping("Heading", "Heading")); 28 | } 29 | 30 | [Test] 31 | public void WithMapDictionary_AddMappings() 32 | { 33 | dict.TryGetValue("Company", out var company); 34 | 35 | dynamicReader.AddMappings(dict); 36 | 37 | Assert.AreEqual(company, dynamicReader.Current.Company); 38 | } 39 | 40 | [Test] 41 | public void WithHeadingMame_RemovesMappings() 42 | { 43 | dict.TryGetValue("Company", out var key); 44 | 45 | dynamicReader.AddMappings(dict); 46 | 47 | var headings = dynamicReader.RemoveMapping("Company").Current; 48 | 49 | foreach (var kvp in headings) 50 | Assert.AreNotEqual(kvp.Key, key); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Swan.Data/Data/IDbTypeMapper.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Data; 2 | 3 | /// 4 | /// Represents a mapper that is able to resolve types between 5 | /// a database provider and CLR types. 6 | /// 7 | public interface IDbTypeMapper 8 | { 9 | /// 10 | /// Gets a list of supported types for this type mapper. 11 | /// 12 | IReadOnlyList SupportedTypes { get; } 13 | 14 | /// 15 | /// Tries to obtain an equivalent for the given CLR type. 16 | /// 17 | /// The CLR type. 18 | /// The . 19 | /// True if the type is found, false otherwise. 20 | bool TryGetProviderTypeFor(Type type, out DbType? dbType); 21 | 22 | /// 23 | /// Tries to obtain a provider-specific type expressed as a DDL string. 24 | /// This method does not take into account precision, scale, or maximum length. 25 | /// 26 | /// The CLR type. 27 | /// The provider-specific data type. 28 | /// True if the type is found, false otherwise. 29 | bool TryGetDatabaseTypeFor(Type type, [MaybeNullWhen(false)] out string providerType); 30 | 31 | /// 32 | /// Tries to obtain a provider-specific column type expressed as a DDL string. 33 | /// This method takes into account precision, scale, or maximum length. 34 | /// 35 | /// The CLR type. 36 | /// The provider-specific data type. 37 | /// True if the type is found, false otherwise. 38 | bool TryGetProviderTypeFor(IDbColumnSchema column, [MaybeNullWhen(false)] out string providerType); 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/Swan.Lite/Swan.Lite.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Repeating code and reinventing the wheel is generally considered bad practice. At Unosquare we are committed to beautiful code and great software. Swan is a collection of classes and extension methods that we and other good developers have developed and evolved over the years. We found ourselves copying and pasting the same code for every project every time we started it. We decide to kill that cycle once and for all. This is the result of that idea. Our philosophy is that SWAN should have no external dependencies, it should be cross-platform, and it should be useful. 5 | Copyright (c) 2016-2022 - Unosquare 6 | Unosquare SWAN 7 | net6.0 8 | true 9 | Swan.Lite 10 | Swan 11 | 4.1.0 12 | Unosquare 13 | https://github.com/unosquare/swan/raw/master/swan-logo-32.png 14 | https://github.com/unosquare/swan 15 | https://raw.githubusercontent.com/unosquare/swan/master/LICENSE 16 | best-practices netcore network objectmapper json-serialization 17 | latest 18 | enable 19 | true 20 | true 21 | AllEnabledByDefault 22 | enable 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Swan.Core/Threading/IWaitEvent.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Threading; 2 | 3 | /// 4 | /// Provides a generalized API for ManualResetEvent and ManualResetEventSlim. 5 | /// 6 | /// 7 | public interface IWaitEvent : IDisposable 8 | { 9 | /// 10 | /// Gets a value indicating whether the event is in the completed state. 11 | /// 12 | bool IsCompleted { get; } 13 | 14 | /// 15 | /// Gets a value indicating whether the Begin method has been called. 16 | /// It returns false after the Complete method is called. 17 | /// 18 | bool IsInProgress { get; } 19 | 20 | /// 21 | /// Returns true if the underlying handle is not closed and it is still valid. 22 | /// 23 | bool IsValid { get; } 24 | 25 | /// 26 | /// Gets a value indicating whether this instance is disposed. 27 | /// 28 | bool IsDisposed { get; } 29 | 30 | /// 31 | /// Enters the state in which waiters need to wait. 32 | /// All future waiters will block when they call the Wait method. 33 | /// 34 | void Begin(); 35 | 36 | /// 37 | /// Leaves the state in which waiters need to wait. 38 | /// All current waiters will continue. 39 | /// 40 | void Complete(); 41 | 42 | /// 43 | /// Waits for the event to be completed. 44 | /// 45 | void Wait(); 46 | 47 | /// 48 | /// Waits for the event to be completed. 49 | /// Returns true when there was no timeout. False if the timeout was reached. 50 | /// 51 | /// The maximum amount of time to wait for. 52 | /// true when there was no timeout. false if the timeout was reached. 53 | bool Wait(TimeSpan timeout); 54 | } 55 | -------------------------------------------------------------------------------- /src/Swan.Logging/Swan.Logging.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Swan 5 | net7.0 6 | latest 7 | enable 8 | true 9 | true 10 | AllEnabledByDefault 11 | enable 12 | ..\..\StyleCop.Analyzers.ruleset 13 | 14 | 15 | 16 | Swan.Logging 17 | 7.0.0-beta.10 18 | mariodivece, geoperez, unosquare 19 | Unosquare 20 | Unosquare SWAN Logging 21 | true 22 | 23 | Logging subsystem and a handy Terminal abstraction. 24 | 25 | Copyright (c) 2016-2022 - Unosquare 26 | MIT 27 | https://github.com/unosquare/swan/ 28 | https://github.com/unosquare/swan 29 | logging terminal swan 30 | swan-logo-32.png 31 | git 32 | 33 | 34 | 35 | 36 | True 37 | \ 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/Swan.Data/Data/Providers/SqliteDbProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Data.Providers; 2 | 3 | internal class SqliteDbProvider : DbProvider 4 | { 5 | public override string ParameterPrefix => "$"; 6 | 7 | public override string SchemaSeparator => string.Empty; 8 | 9 | public override IDbTypeMapper TypeMapper { get; } = new SqliteTypeMapper(); 10 | 11 | public override Func ColumnSchemaFactory { get; } = () => new SqliteColumn(); 12 | 13 | public override DbCommand CreateListTablesCommand(DbConnection connection) => 14 | connection is null 15 | ? throw new ArgumentNullException(nameof(connection)) 16 | : connection 17 | .BeginCommandText("SELECT name AS [Name], '' AS [Schema], IIF(type = 'view', 1, 0) AS [IsView] FROM (SELECT * FROM sqlite_schema UNION ALL SELECT * FROM sqlite_temp_schema) WHERE type = 'table' OR type = 'view' ORDER BY name") 18 | .EndCommandText(); 19 | 20 | public override string? GetColumnDdlString(IDbColumnSchema column) => 21 | column is null 22 | ? throw new ArgumentNullException(nameof(column)) 23 | : column.IsIdentity && column.DataType.TypeInfo().IsNumeric 24 | ? $"{QuoteField(column.ColumnName),16} INTEGER PRIMARY KEY" 25 | : base.GetColumnDdlString(column); 26 | 27 | public override bool TryGetSelectLastInserted(IDbTableSchema table, out string? commandText) 28 | { 29 | commandText = null; 30 | if (table.IdentityKeyColumn is null || table.KeyColumns.Count != 1) 31 | return false; 32 | 33 | var quotedFields = string.Join(", ", table.Columns.Select(c => QuoteField(c.ColumnName))); 34 | var quotedTable = QuoteTable(table.TableName, table.Schema); 35 | 36 | commandText = $"SELECT {quotedFields} FROM {quotedTable} WHERE _rowid_ = last_insert_rowid() LIMIT 1"; 37 | return true; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Swan.Threading/Threading/CancellationTokenOwner.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Threading; 2 | 3 | /// 4 | /// Acts as a but with reusable tokens. 5 | /// 6 | public sealed class CancellationTokenOwner : IDisposable 7 | { 8 | private readonly object _syncLock = new(); 9 | private bool _isDisposed; 10 | private CancellationTokenSource _tokenSource = new(); 11 | 12 | /// 13 | /// Gets the token of the current. 14 | /// 15 | public CancellationToken Token 16 | { 17 | get 18 | { 19 | lock (_syncLock) 20 | { 21 | return _isDisposed 22 | ? CancellationToken.None 23 | : _tokenSource.Token; 24 | } 25 | } 26 | } 27 | 28 | /// 29 | /// Cancels the last referenced token and creates a new token source. 30 | /// 31 | public void Cancel() 32 | { 33 | lock (_syncLock) 34 | { 35 | if (_isDisposed) return; 36 | _tokenSource.Cancel(); 37 | _tokenSource.Dispose(); 38 | _tokenSource = new(); 39 | } 40 | } 41 | 42 | /// 43 | public void Dispose() => Dispose(true); 44 | 45 | /// 46 | /// Releases unmanaged and - optionally - managed resources. 47 | /// 48 | /// true to release both managed and unmanaged resources; false to release only unmanaged resources. 49 | private void Dispose(bool disposing) 50 | { 51 | lock (_syncLock) 52 | { 53 | if (_isDisposed) return; 54 | 55 | if (disposing) 56 | { 57 | _tokenSource.Cancel(); 58 | _tokenSource.Dispose(); 59 | } 60 | 61 | _isDisposed = true; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Swan.Artifacts/Swan.Artifacts.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Swan.Artifacts 5 | net6.0 6 | latest 7 | enable 8 | true 9 | true 10 | AllEnabledByDefault 11 | enable 12 | 13 | 14 | 15 | Repeating code and reinventing the wheel is generally considered bad practice. At Unosquare we are committed to beautiful code and great software. Swan is a collection of classes and extension methods that we and other good developers have developed and evolved over the years. We found ourselves copying and pasting the same code for every project every time we started it. We decide to kill that cycle once and for all. This is the result of that idea. Our philosophy is that SWAN should have no external dependencies, it should be cross-platform, and it should be useful. 16 | Copyright (c) 2016-2022 - Unosquare 17 | Unosquare SWAN 18 | true 19 | Swan 20 | 5.0.1 21 | Unosquare 22 | https://github.com/unosquare/swan/raw/master/swan-logo-32.png 23 | https://github.com/unosquare/swan 24 | https://raw.githubusercontent.com/unosquare/swan/master/LICENSE 25 | best-practices netcore network objectmapper json-serialization 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/Swan.Lite/Collections/CollectionCacheRepository.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Collections; 2 | 3 | using System; 4 | using System.Collections.Concurrent; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | /// 9 | /// A thread-safe collection cache repository for types. 10 | /// 11 | /// The type of member to cache. 12 | public class CollectionCacheRepository 13 | { 14 | private readonly Lazy>> _data = 15 | new(() => 16 | new(), true); 17 | 18 | /// 19 | /// Determines whether the cache contains the specified key. 20 | /// 21 | /// The key. 22 | /// true if the cache contains the key, otherwise false. 23 | public bool ContainsKey(Type key) => _data.Value.ContainsKey(key); 24 | 25 | /// 26 | /// Retrieves the properties stored for the specified type. 27 | /// If the properties are not available, it calls the factory method to retrieve them 28 | /// and returns them as an array of PropertyInfo. 29 | /// 30 | /// The key. 31 | /// The factory. 32 | /// 33 | /// An array of the properties stored for the specified type. 34 | /// 35 | /// 36 | /// key 37 | /// or 38 | /// factory. 39 | /// 40 | public IEnumerable Retrieve(Type key, Func> factory) 41 | { 42 | if (key == null) 43 | throw new ArgumentNullException(nameof(key)); 44 | 45 | if (factory == null) 46 | throw new ArgumentNullException(nameof(factory)); 47 | 48 | return _data.Value.GetOrAdd(key, k => factory.Invoke(k).Where(item => item != null)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/Swan.Test/TextSerializerTest.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test; 2 | 3 | using Formatters; 4 | using Mocks; 5 | using System.Text.Json; 6 | 7 | [TestFixture] 8 | public class StringSerialize 9 | { 10 | [Test] 11 | public void WithString_ReturnsString() => Assert.AreEqual(@"""Hola""", TextSerializer.Serialize("Hola")); 12 | 13 | [Test] 14 | public void WithNumber_ReturnsNumberRepresentation() => Assert.AreEqual(@"1", TextSerializer.Serialize(1)); 15 | 16 | [Test] 17 | public void WithBoolean_ReturnsBoolRepresentation() => Assert.AreEqual("true", TextSerializer.Serialize(true)); 18 | 19 | [Test] 20 | public void WithObject_ReturnsObjectRepresentation() => Assert.AreEqual("{\r\n"+@" ""Message"": null"+"\r\n}", 21 | TextSerializer.Serialize(new ErrorJson())); 22 | 23 | [Test] 24 | public void WithDictionary_ReturnsRepresentation() 25 | { 26 | var dict = new Dictionary 27 | { 28 | { 1, "One" }, 29 | { 2, "Two" }, 30 | { 3, "Three" } 31 | }; 32 | 33 | var serialized = TextSerializer.Serialize(dict); 34 | 35 | Assert.AreEqual("{\r\n \"1\": \"One\",\r\n \"2\": \"Two\",\r\n \"3\": \"Three\"\r\n}", serialized); 36 | } 37 | 38 | [Test] 39 | public void WithArray_ReturnsRepresentation() 40 | { 41 | var intARray = new int[] 42 | { 43 | 1,2,3 44 | }; 45 | 46 | var serialized = TextSerializer.Serialize(intARray); 47 | 48 | Assert.AreEqual("[1, 2, 3]", serialized); 49 | } 50 | 51 | [Test] 52 | public void WithJson_ReturnsRepresentation() 53 | { 54 | var jd = JsonDocument.Parse("{\"CarModel\":\"Vocho\"}"); 55 | JsonElement je = jd.RootElement; 56 | 57 | var serialized = TextSerializer.Serialize(je); 58 | 59 | Assert.AreEqual("{\r\n \"CarModel\": \"Vocho\"\r\n}", serialized); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Swan.Data/Swan.Data.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Swan 5 | net7.0 6 | latest 7 | enable 8 | enable 9 | true 10 | true 11 | AllEnabledByDefault 12 | ..\..\StyleCop.Analyzers.ruleset 13 | 14 | 15 | 16 | Swan.Data 17 | 7.0.0-beta.9 18 | mariodivece, geoperez, unosquare 19 | Unosquare 20 | Unosquare SWAN Data 21 | true 22 | 23 | Provides data operations and schema information on database connections. 24 | 25 | Copyright (c) 2016-2022 - Unosquare 26 | MIT 27 | https://github.com/unosquare/swan/ 28 | https://github.com/unosquare/swan 29 | swan library data schema poco query 30 | swan-logo-32.png 31 | git 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | True 45 | \ 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /.github/workflows/lib-publish-nuget-org.yml: -------------------------------------------------------------------------------- 1 | name: Lib - Build and Push (Nuget.org) 2 | on: 3 | workflow_call: 4 | secrets: 5 | nuget-api-key: 6 | description: 'The Nuget.org publish API key to push the Nuget package.' 7 | required: true 8 | inputs: 9 | project-name: 10 | description: 'The name of the project to build. Must match the directory name.' 11 | required: true 12 | type: string 13 | base-path: 14 | description: 'The working directory base path with a trailing forward-slash' 15 | required: false 16 | default: '/home/runner/work/${{ github.event.repository.name }}/${{ github.event.repository.name }}/' 17 | type: string 18 | source-path: 19 | description: > 20 | The source path relative to the base path with a trailing forward-slash. 21 | For example, if the project folder is in a subfolder called src, then this value needs to be set to src/ 22 | If there is no subfolder, just leave it empty. 23 | required: false 24 | default: '' 25 | type: string 26 | jobs: 27 | lib-publish-nuget: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: 🧑‍💻 Checkout codebase 31 | uses: actions/checkout@v4 32 | - name: 🥓 Setup .NET 33 | uses: actions/setup-dotnet@v4 34 | with: 35 | dotnet-version: | 36 | 5.0.x 37 | 6.0.x 38 | 7.0.x 39 | 8.0.x 40 | - name: 🏗 Build, Pack and Publish to Nuget.org 41 | run: | 42 | dotnet pack ${{ inputs.base-path }}${{ inputs.source-path }}${{ inputs.project-name }}/${{ inputs.project-name }}.csproj --configuration Release -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg 43 | dotnet nuget push ${{ inputs.base-path }}${{ inputs.source-path }}${{ inputs.project-name }}/bin/Release/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.nuget-api-key }} 44 | -------------------------------------------------------------------------------- /test/Swan.Test/CsvDictionaryReaderTest.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test; 2 | 3 | using NUnit.Framework; 4 | using Formatters; 5 | using System.Text; 6 | 7 | [TestFixture] 8 | public class CsvDictionaryReaderTest 9 | { 10 | private const string Data = @"Company,OpenPositions,MainTechnology,Revenue 11 | Co,2,""C#, MySQL, JavaScript, HTML5 and CSS3"","" $1,359,885 "" 12 | Ca,2,""C#, MySQL, JavaScript, HTML5 and CSS3"","" $1,359,885 """; 13 | 14 | private static readonly MemoryStream stream = new(Encoding.ASCII.GetBytes(Data)); 15 | private readonly CsvDictionaryReader dictionaryReader = new(stream); 16 | private readonly Dictionary dict = new() 17 | { 18 | { "Company", "Company" }, 19 | }; 20 | 21 | [Test] 22 | public void WithNullValues_ThrowsException() 23 | { 24 | Assert.Throws(() => dictionaryReader.AddMappings(null)); 25 | Assert.Throws(() => dictionaryReader.AddMapping(null, null)); 26 | Assert.Throws(() => dictionaryReader.AddMapping("Company", null)); 27 | Assert.Throws(() => dictionaryReader.AddMapping("Heading", "Heading")); 28 | } 29 | 30 | [Test] 31 | public void WithMapDictionary_AddMapings() 32 | { 33 | dict.TryGetValue("Company", out var company); 34 | 35 | dictionaryReader.AddMappings(dict); 36 | 37 | dictionaryReader.Current.TryGetValue("Company", out var addedMap); 38 | 39 | Assert.AreEqual(company, addedMap); 40 | } 41 | 42 | [Test] 43 | public void WithHeadingName_RemovesMappings() 44 | { 45 | dict.TryGetValue("Company", out var key); 46 | 47 | dictionaryReader.AddMappings(dict); 48 | 49 | var headings = dictionaryReader.RemoveMapping("Company").Current; 50 | 51 | foreach (var kvp in headings) 52 | { 53 | Assert.AreNotEqual(kvp.Key, key); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Swan.Data/Data/Extensions/DataTableExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Data.Extensions; 2 | 3 | /// 4 | /// Provides extensions for objects. 5 | /// 6 | public static class DataTableExtensions 7 | { 8 | /// 9 | /// Converts a object into an enumerable set 10 | /// of objects of the given type with property names corresponding to columns. 11 | /// 12 | /// The data table to extract rows from. 13 | /// An optional deserializer method that produces an object by providing a data row. 14 | /// An enumerable set of objects. 15 | public static IEnumerable Query(this DataTable table, Func? deserialize = default) 16 | { 17 | if (table is null) 18 | throw new ArgumentNullException(nameof(table)); 19 | 20 | deserialize ??= (r) => r.ToDataRecord().ParseObject(); 21 | 22 | foreach (DataRow row in table.Rows) 23 | if (row is not null) 24 | yield return deserialize.Invoke(row); 25 | } 26 | 27 | /// 28 | /// Converts a object into an enumerable set 29 | /// of with property names corresponding to columns. 30 | /// Property names are normalized by removing whitespace, special 31 | /// characters or leading digits. 32 | /// 33 | /// The data table to extract rows from. 34 | /// An enumerable set of dynamically typed Expando objects. 35 | public static IEnumerable Query(this DataTable table) 36 | { 37 | if (table is null) 38 | throw new ArgumentNullException(nameof(table)); 39 | 40 | foreach (DataRow row in table.Rows) 41 | if (row is not null) 42 | yield return row.ToDataRecord().ParseExpando(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Swan.Core/Formatters/JsonFormatter.Dynamic.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Formatters; 2 | 3 | using System.IO; 4 | using System.Text.Json; 5 | 6 | public static partial class JsonFormatter 7 | { 8 | /// 9 | /// Deserializes a JSON string into a dynamic object. 10 | /// 11 | /// The string to parse. Has to be a valid JSON document. 12 | /// Optional JSON document options. 13 | /// Optional JSON element value parser. 14 | /// The deserialized dynamic object. 15 | public static dynamic? JsonDeserialize(this string @this, JsonDocumentOptions options = default, Func? valueParser = default) 16 | { 17 | if (string.IsNullOrWhiteSpace(@this)) 18 | return null; 19 | 20 | using var jsonDocument = JsonDocument.Parse(@this, options); 21 | var jsonObject = new JsonDynamicObject(jsonDocument.RootElement, valueParser); 22 | return jsonObject.Materialize(); 23 | } 24 | 25 | /// 26 | /// Deserializes a JSON stream of UTF8 bytes into a dynamic object. 27 | /// 28 | /// The stream of bytes in UTF8. 29 | /// Optional JSON document options. 30 | /// Optional JSON element value parser. 31 | /// The deserialized dynamic object. 32 | public static async Task JsonDeserializeAsync(this Stream? @this, JsonDocumentOptions options = default, Func? valueParser = default) 33 | { 34 | if (@this is null) 35 | return null; 36 | 37 | using var jsonDocument = await JsonDocument 38 | .ParseAsync(@this, options) 39 | .ConfigureAwait(false); 40 | 41 | var jsonObject = new JsonDynamicObject(jsonDocument.RootElement, valueParser); 42 | return jsonObject.Materialize(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Swan.Data/Data/Records/PropertyColumnMap.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Data.Records; 2 | 3 | /// 4 | /// Provides column-to-property maps for efficient object parsing. 5 | /// 6 | internal static class PropertyColumnMap 7 | { 8 | private static readonly object SyncLock = new(); 9 | private static readonly Dictionary> PropertyColumnMaps = new(128); 10 | 11 | /// 12 | /// Gets a dictionary in which keys are the names of the columns and the values represent the property proxies. 13 | /// 14 | /// The type info to get the column map for. 15 | /// The column map as a dictionary. 16 | public static IReadOnlyDictionary GetColumnMap(this ITypeInfo typeInfo) 17 | { 18 | lock (SyncLock) 19 | { 20 | if (PropertyColumnMaps.TryGetValue(typeInfo, out var map)) 21 | return map; 22 | 23 | map = new(StringComparer.OrdinalIgnoreCase); 24 | foreach (var (propertyName, property) in typeInfo.Properties) 25 | { 26 | var fieldName = property.Attribute() is ColumnAttribute columnAttribute 27 | && !string.IsNullOrWhiteSpace(columnAttribute.Name) 28 | ? columnAttribute.Name 29 | : propertyName.Contains('.', StringComparison.Ordinal) 30 | ? propertyName.Split('.', StringSplitOptions.RemoveEmptyEntries)[^1] 31 | : property.PropertyName; 32 | 33 | fieldName = fieldName.Trim(); 34 | 35 | if (string.IsNullOrWhiteSpace(fieldName)) 36 | continue; 37 | 38 | if (map.ContainsKey(fieldName)) 39 | continue; 40 | 41 | map[fieldName] = property; 42 | } 43 | 44 | PropertyColumnMaps.Add(typeInfo, map); 45 | return map; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/Swan.Test/Mocks/CollectionSamples.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test.Mocks; 2 | 3 | using System.Collections; 4 | using System.Collections.ObjectModel; 5 | 6 | public static class CollectionSamples 7 | { 8 | private static readonly Dictionary BaseCollection = new() 9 | { 10 | ["item 1"] = 1, 11 | ["item 2"] = 2, 12 | ["item 3"] = 3, 13 | ["item 4"] = 4, 14 | ["item 5"] = 5, 15 | ["item 6"] = 6, 16 | ["item 7"] = 7, 17 | ["item 8"] = 8, 18 | }; 19 | 20 | public static IEnumerable ReadOnlyDictionary => new ReadOnlyDictionary(BaseCollection); 21 | 22 | public static IEnumerable GenericDictionary => BaseCollection.ToDictionary(c => c.Key, c => c.Value); 23 | 24 | public static IEnumerable Dictionary => new Hashtable(BaseCollection); 25 | 26 | public static IEnumerable ReadOnlyList => new ArraySegment(BaseCollection.Values.ToArray()); 27 | 28 | public static IEnumerable GenericList => BaseCollection.Values.Select(c => c).ToList(); 29 | 30 | public static IEnumerable List => new ArrayList(BaseCollection.Values.ToArray()); 31 | 32 | public static IEnumerable ReadOnlyCollection => new ReadOnlyCollection(BaseCollection.Values.ToList()); 33 | 34 | public static IEnumerable GenericCollection => new HashSet(BaseCollection.Values.ToArray()); 35 | 36 | public static IEnumerable Collection 37 | { 38 | get 39 | { 40 | var q = new Queue(); 41 | foreach (var item in BaseCollection.Values) 42 | q.Enqueue(item); 43 | 44 | return q; 45 | } 46 | } 47 | 48 | public static IEnumerable GenericEnumerable => BaseCollection.Values.Select(c => c); 49 | 50 | public static IEnumerable Enumerable => new SimpleEnumerable(); 51 | 52 | public static IEnumerable Array => "Hello World!".ToArray(); 53 | 54 | private class SimpleEnumerable : IEnumerable 55 | { 56 | public IEnumerator GetEnumerator() => BaseCollection.Values.GetEnumerator(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Swan.Core/Gizmos/AsyncLazy.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Gizmos; 2 | 3 | /// 4 | /// An asynchronous version of the construct 5 | /// with embedded thread safety. 6 | /// 7 | /// The type of object the factory method produces. 8 | public class AsyncLazy 9 | where T : class 10 | { 11 | private readonly SemaphoreSlim FactorySemaphore = new(1, 1); 12 | private readonly Func> TaskFactory; 13 | private long creationFlag; 14 | private T? FactoryValue; 15 | 16 | /// 17 | /// Creates a new instance of the class. 18 | /// 19 | /// The factory method that produces the typed object. 20 | public AsyncLazy(Func> factory) => TaskFactory = factory; 21 | 22 | /// 23 | /// Gets a value indicating whether the value has been created. 24 | /// 25 | public bool IsValueCreated => Interlocked.Read(ref creationFlag) != 0; 26 | 27 | /// 28 | /// Asynchronously uses the provided factory method to produce a value 29 | /// for the first time. If the value is already created, it simply returns the previously 30 | /// created value. 31 | /// 32 | /// The cancellation token. 33 | /// An awaitable task. 34 | public async Task GetValueAsync(CancellationToken ct = default) 35 | { 36 | if (IsValueCreated) 37 | return FactoryValue!; 38 | 39 | try 40 | { 41 | await FactorySemaphore.WaitAsync(ct).ConfigureAwait(false); 42 | 43 | if (FactoryValue is null && !IsValueCreated) 44 | { 45 | FactoryValue = await TaskFactory.Invoke().ConfigureAwait(false); 46 | Interlocked.Increment(ref creationFlag); 47 | } 48 | 49 | return FactoryValue!; 50 | } 51 | finally 52 | { 53 | FactorySemaphore.Release(); 54 | } 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /src/Swan.Data.SqlBulkOps/Swan.Data.SqlBulkOps.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Swan.Data.SqlBulkOps 5 | net7.0 6 | latest 7 | enable 8 | enable 9 | true 10 | true 11 | AllEnabledByDefault 12 | ..\..\StyleCop.Analyzers.ruleset 13 | 14 | 15 | 16 | Swan.Data.SqlBulkOps 17 | 7.0.0-beta.8 18 | mariodivece, geoperez, unosquare 19 | Unosquare 20 | Unosquare SWAN Data 21 | true 22 | 23 | Provides SQL Server Bulk operations as a plug-in to Swan's data framework. 24 | 25 | Copyright (c) 2016-2022 - Unosquare 26 | MIT 27 | https://github.com/unosquare/swan/ 28 | https://github.com/unosquare/swan 29 | swan library data schema poco query 30 | swan-logo-32.png 31 | git 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | True 45 | \ 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/Swan.Threading/Swan.Threading.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Swan.Threading 5 | net7.0 6 | latest 7 | enable 8 | true 9 | true 10 | AllEnabledByDefault 11 | enable 12 | 13 | 14 | 15 | Repeating code and reinventing the wheel is generally considered bad practice. At Unosquare we are committed to beautiful code and great software. Swan is a collection of classes and extension methods that we and other good developers have developed and evolved over the years. We found ourselves copying and pasting the same code for every project every time we started it. We decide to kill that cycle once and for all. This is the result of that idea. Our philosophy is that SWAN should have no external dependencies, it should be cross-platform, and it should be useful. 16 | Copyright (c) 2016-2022 - Unosquare 17 | Unosquare SWAN 18 | true 19 | Swan 20 | 7.0.0-beta.1 21 | Unosquare 22 | https://github.com/unosquare/swan/raw/master/swan-logo-32.png 23 | https://github.com/unosquare/swan 24 | https://raw.githubusercontent.com/unosquare/swan/master/LICENSE 25 | best-practices netcore network objectmapper json-serialization 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/Swan.Net/JsonRequestException.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Net; 2 | 3 | using System; 4 | using System.Net.Http; 5 | 6 | /// 7 | /// Represents errors that occurs requesting a JSON file through HTTP. 8 | /// 9 | /// 10 | [Serializable] 11 | public class JsonRequestException 12 | : Exception 13 | { 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// The URL. 18 | /// The method. 19 | /// The HTTP error code. 20 | /// Content of the error. 21 | public JsonRequestException(Uri url, HttpMethod method, int httpErrorCode = 500, string? errorContent = null) 22 | : base($"HTTP Request Error Url {url} Method {method} HTTP Status Code {httpErrorCode} Body Content {errorContent}") 23 | { 24 | Url = url; 25 | Method = method; 26 | HttpErrorCode = httpErrorCode; 27 | HttpErrorContent = errorContent; 28 | } 29 | 30 | /// 31 | /// Gets the URL. 32 | /// 33 | /// 34 | /// The URL. 35 | /// 36 | public Uri Url { get; } 37 | 38 | /// 39 | /// Gets the method. 40 | /// 41 | /// 42 | /// The method. 43 | /// 44 | public HttpMethod Method { get; } 45 | 46 | /// 47 | /// Gets the HTTP error code. 48 | /// 49 | /// 50 | /// The HTTP error code. 51 | /// 52 | public int HttpErrorCode { get; } 53 | 54 | /// 55 | /// Gets the content of the HTTP error. 56 | /// 57 | /// 58 | /// The content of the HTTP error. 59 | /// 60 | public string? HttpErrorContent { get; } 61 | 62 | /// 63 | public override string ToString() => string.IsNullOrEmpty(HttpErrorContent) 64 | ? $"HTTP Response Status Code {HttpErrorCode} Error Message: {HttpErrorContent}" 65 | : base.ToString(); 66 | } 67 | -------------------------------------------------------------------------------- /src/Swan.Data/Data/Providers/MySqlDbProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Data.Providers; 2 | 3 | internal class MySqlDbProvider : DbProvider 4 | { 5 | public override IDbTypeMapper TypeMapper { get; } = new MySqlTypeMapper(); 6 | 7 | public override string QuotePrefix => "`"; 8 | 9 | public override string QuoteSuffix => "`"; 10 | 11 | public override string SchemaSeparator => string.Empty; 12 | 13 | public override DbCommand CreateListTablesCommand(DbConnection connection) 14 | { 15 | if (connection is null) 16 | throw new ArgumentNullException(nameof(connection)); 17 | 18 | var database = connection.Database; 19 | 20 | return connection 21 | .BeginCommandText( 22 | $"SELECT `table_name` AS `Name`, '' AS `Schema`, IF(`table_type` = 'VIEW', 1, 0) AS `IsView` FROM `information_schema`.`tables` WHERE `table_schema` = {QuoteParameter(nameof(database))}") 23 | .EndCommandText() 24 | .SetParameter(nameof(database), database); 25 | } 26 | 27 | public override string? GetColumnDdlString(IDbColumnSchema column) => column is null 28 | ? throw new ArgumentNullException(nameof(column)) 29 | : !TypeMapper.TryGetProviderTypeFor(column, out var providerType) 30 | ? default 31 | : column.IsIdentity && column.DataType.TypeInfo().IsNumeric 32 | ? $"{QuoteField(column.ColumnName),16} {providerType} NOT NULL AUTO_INCREMENT" 33 | : base.GetColumnDdlString(column); 34 | 35 | public override bool TryGetSelectLastInserted(IDbTableSchema table, out string? commandText) 36 | { 37 | commandText = null; 38 | if (table.IdentityKeyColumn is null || table.KeyColumns.Count != 1) 39 | return false; 40 | 41 | var quotedFields = string.Join(", ", table.Columns.Select(c => QuoteField(c.ColumnName))); 42 | var quotedTable = QuoteTable(table.TableName, table.Schema); 43 | var quotedKeyField = QuoteField(table.IdentityKeyColumn.ColumnName); 44 | 45 | commandText = $"SELECT {quotedFields} FROM {quotedTable} WHERE {quotedKeyField} = LAST_INSERT_ID() LIMIT 1"; 46 | return true; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Swan.Lite/SingletonBase.cs: -------------------------------------------------------------------------------- 1 | namespace Swan; 2 | 3 | using Reflection; 4 | using System; 5 | 6 | /// 7 | /// Represents a singleton pattern abstract class. 8 | /// 9 | /// The type of class. 10 | public abstract class SingletonBase : IDisposable 11 | where T : class 12 | { 13 | /// 14 | /// The static, singleton instance reference. 15 | /// 16 | protected static readonly Lazy LazyInstance = new( 17 | valueFactory: () => TypeManager.CreateInstance() 18 | ?? throw new MissingMethodException(typeof(T).Name, ".ctor"), 19 | isThreadSafe: true); 20 | 21 | private bool _isDisposing; // To detect redundant calls 22 | 23 | /// 24 | /// Gets the instance that this singleton represents. 25 | /// If the instance is null, it is constructed and assigned when this member is accessed. 26 | /// 27 | /// 28 | /// The instance. 29 | /// 30 | public static T Instance => LazyInstance.Value; 31 | 32 | /// 33 | public void Dispose() => Dispose(true); 34 | 35 | /// 36 | /// Releases unmanaged and - optionally - managed resources. 37 | /// Call the GC.SuppressFinalize if you override this method and use 38 | /// a non-default class finalizer (destructor). 39 | /// 40 | /// true to release both managed and unmanaged resources; false to release only unmanaged resources. 41 | protected virtual void Dispose(bool disposeManaged) 42 | { 43 | if (_isDisposing) return; 44 | 45 | _isDisposing = true; 46 | 47 | // free managed resources 48 | if (LazyInstance == null) return; 49 | 50 | try 51 | { 52 | var disposableInstance = LazyInstance.Value as IDisposable; 53 | disposableInstance?.Dispose(); 54 | } 55 | #pragma warning disable CA1031 // Do not catch general exception types 56 | catch 57 | #pragma warning restore CA1031 // Do not catch general exception types 58 | { 59 | // swallow 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Swan.Threading/Threading/IWorker.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Threading; 2 | 3 | /// 4 | /// Defines a standard API to control background application workers. 5 | /// 6 | public interface IWorker 7 | { 8 | /// 9 | /// Gets the current state of the worker. 10 | /// 11 | WorkerState WorkerState { get; } 12 | 13 | /// 14 | /// Gets a value indicating whether this instance is disposed. 15 | /// 16 | /// 17 | /// true if this instance is disposed; otherwise, false. 18 | /// 19 | bool IsDisposed { get; } 20 | 21 | /// 22 | /// Gets a value indicating whether this instance is currently being disposed. 23 | /// 24 | /// 25 | /// true if this instance is disposing; otherwise, false. 26 | /// 27 | bool IsDisposing { get; } 28 | 29 | /// 30 | /// Gets or sets the time interval used to execute cycles. 31 | /// 32 | TimeSpan Period { get; set; } 33 | 34 | /// 35 | /// Gets the name identifier of this worker. 36 | /// 37 | string Name { get; } 38 | 39 | /// 40 | /// Starts execution of worker cycles. 41 | /// 42 | /// The awaitable task. 43 | Task StartAsync(); 44 | 45 | /// 46 | /// Pauses execution of worker cycles. 47 | /// 48 | /// The awaitable task. 49 | Task PauseAsync(); 50 | 51 | /// 52 | /// Resumes execution of worker cycles. 53 | /// 54 | /// The awaitable task. 55 | Task ResumeAsync(); 56 | 57 | /// 58 | /// Permanently stops execution of worker cycles. 59 | /// An interrupt is always sent to the worker. If you wish to stop 60 | /// the worker without interrupting then call the 61 | /// method, await it, and finally call the method. 62 | /// 63 | /// The awaitable task. 64 | Task StopAsync(); 65 | } 66 | -------------------------------------------------------------------------------- /src/Swan.Core/Swan.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Swan 5 | net7.0 6 | latest 7 | enable 8 | enable 9 | true 10 | true 11 | AllEnabledByDefault 12 | ..\..\StyleCop.Analyzers.ruleset 13 | 14 | 15 | 16 | Swan.Core 17 | 7.0.0-beta.12 18 | mariodivece, geoperez, unosquare 19 | Unosquare 20 | Unosquare SWAN Core 21 | true 22 | 23 | Repeating code and reinventing the wheel is generally considered bad practice. 24 | At Unosquare we are committed to beautiful code and great software. SWAN is a collection of classes and 25 | extension methods that we and other good developers have developed and evolved over the years. 26 | We found ourselves copying and pasting the same code for every project every time we started it. We decided 27 | to kill that cycle once and for all. This is the result of that idea. Our philosophy is that SWAN should 28 | have no external dependencies, it should be cross-platform, and it should be useful. 29 | 30 | Copyright (c) 2016-2022 - Unosquare 31 | MIT 32 | https://github.com/unosquare/swan/ 33 | https://github.com/unosquare/swan 34 | best-practices netcore network objectmapper json-serialization utilities reflection 35 | swan-logo-32.png 36 | git 37 | 38 | 39 | 40 | 41 | True 42 | \ 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/Swan.Net/Extensions/NetworkExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Net.Extensions; 2 | 3 | using System; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Net.Sockets; 7 | 8 | /// 9 | /// Provides various extension methods for networking-related tasks. 10 | /// 11 | public static class NetworkExtensions 12 | { 13 | /// 14 | /// Determines whether the IP address is private. 15 | /// 16 | /// The IP address. 17 | /// 18 | /// True if the IP Address is private; otherwise, false. 19 | /// 20 | /// address. 21 | public static bool IsPrivateAddress(this IPAddress @this) 22 | { 23 | if (@this == null) 24 | throw new ArgumentNullException(nameof(@this)); 25 | 26 | var octets = @this.ToString().Split(new[] { "." }, StringSplitOptions.RemoveEmptyEntries).Select(byte.Parse).ToArray(); 27 | var is24Bit = octets[0] == 10; 28 | var is20Bit = octets[0] == 172 && (octets[1] >= 16 && octets[1] <= 31); 29 | var is16Bit = octets[0] == 192 && octets[1] == 168; 30 | 31 | return is24Bit || is20Bit || is16Bit; 32 | } 33 | 34 | /// 35 | /// Converts an IPv4 Address to its Unsigned, 32-bit integer representation. 36 | /// 37 | /// The address. 38 | /// 39 | /// A 32-bit unsigned integer converted from four bytes at a specified position in a byte array. 40 | /// 41 | /// address. 42 | /// InterNetwork - address. 43 | public static uint ToUInt32(this IPAddress @this) 44 | { 45 | if (@this == null) 46 | throw new ArgumentNullException(nameof(@this)); 47 | 48 | if (@this.AddressFamily != AddressFamily.InterNetwork) 49 | throw new ArgumentException($"Address has to be of family '{nameof(AddressFamily.InterNetwork)}'", nameof(@this)); 50 | 51 | var addressBytes = @this.GetAddressBytes(); 52 | if (BitConverter.IsLittleEndian) 53 | Array.Reverse(addressBytes); 54 | 55 | return BitConverter.ToUInt32(addressBytes, 0); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/Swan.Test/CodeGenExtensionTests.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test; 2 | 3 | using Mocks; 4 | 5 | [TestFixture] 6 | public class CodeGenExtensionTest 7 | { 8 | [Test] 9 | public void CreateTableAndGetItsPocoCode() 10 | { 11 | var conn = new SqliteConnection("Data Source=:memory:"); 12 | var table = conn.TableBuilder("Projects").ExecuteTableCommand(); 13 | var pocoCode = table.GeneratePocoCode(); 14 | 15 | Assert.IsTrue(pocoCode.Contains($"[Table(\"{table.TableName}\", Schema = \"{table.Schema}\")]")); 16 | 17 | Assert.IsTrue(pocoCode.Contains($"Column(nameof({table.Columns[0].ColumnName})")); 18 | Assert.IsTrue(pocoCode.Contains($"Column(nameof({table.Columns[1].ColumnName})")); 19 | Assert.IsTrue(pocoCode.Contains($"Column(nameof({table.Columns[2].ColumnName})")); 20 | Assert.IsTrue(pocoCode.Contains($"Column(nameof({table.Columns[3].ColumnName})")); 21 | Assert.IsTrue(pocoCode.Contains($"Column(nameof({table.Columns[4].ColumnName})")); 22 | Assert.IsTrue(pocoCode.Contains($"Column(nameof({table.Columns[5].ColumnName})")); 23 | Assert.IsTrue(pocoCode.Contains($"Column(nameof({table.Columns[6].ColumnName})")); 24 | Assert.IsTrue(pocoCode.Contains($"Column(nameof({table.Columns[7].ColumnName})")); 25 | } 26 | 27 | [Test] 28 | public void CreateTableWithSchemaAndEntityNameAndGetItsPocoCode() 29 | { 30 | var conn = new SqliteConnection("Data Source=:memory:"); 31 | var table = conn.TableBuilder("Projects", "Main").ExecuteTableCommand(); 32 | var pocoCode = table.GeneratePocoCode("Project"); 33 | 34 | Assert.IsTrue(pocoCode.Contains($"[Table(\"{table.TableName}\", Schema = \"Main\")]")); 35 | } 36 | 37 | [Test] 38 | public void TryGetPocoCodeWhenTableIsNull() 39 | { 40 | Data.Context.ITableContext? table = null; 41 | 42 | Assert.Throws(() => table.GeneratePocoCode()); 43 | } 44 | 45 | [Test] 46 | public void TryGetPocoCodeWhenTableHasNoColumns() 47 | { 48 | var conn = new SqliteConnection("Data Source=:memory:"); 49 | var table = conn.TableBuilder("ProjectsNoColumns"); 50 | table.RemoveColumn("ProjectId"); 51 | 52 | Assert.Throws(() => table.GeneratePocoCode()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Swan.Samples/Models.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Samples; 2 | 3 | public interface ITenantRecord 4 | { 5 | /// 6 | /// Gets the unique id of a tenant. 7 | /// 8 | Guid TenantId { get; } 9 | 10 | /// 11 | /// Gets the column name of the record identifier. 12 | /// 13 | string RecordIdFieldName { get; } 14 | 15 | /// 16 | /// Gets the value of the unique record identifier within a tenant. 17 | /// 18 | object RecordIdValue { get; } 19 | } 20 | 21 | public interface ITenantRecord : ITenantRecord 22 | { 23 | /// 24 | /// Gets the typed value of the unique record id. 25 | /// 26 | new T RecordIdValue { get; } 27 | } 28 | 29 | public record Organisation : ITenantRecord 30 | { 31 | public Guid TenantId { get; set; } 32 | 33 | public Guid OrganisationId { get; set; } 34 | 35 | public string? ApiKey { get; set; } 36 | 37 | public string? Name { get; set; } 38 | 39 | public bool PaysTax { get; set; } 40 | 41 | public string? Version { get; set; } 42 | 43 | public string? OrganisationType { get; set; } 44 | 45 | public string? OrganisationEntityType { get; set; } 46 | 47 | public string? BaseCurrency { get; set; } 48 | 49 | public string? CountryCode { get; set; } 50 | 51 | public bool IsDemoCompany { get; set; } 52 | 53 | public string? OrganisationStatus { get; set; } 54 | 55 | public string? RegistrationNumber { get; set; } 56 | 57 | public string? EmployerIdentificationNumber { get; set; } 58 | 59 | public string? TaxNumber { get; set; } 60 | 61 | public int FinancialYearEndDay { get; set; } 62 | 63 | public int FinancialYearEndMonth { get; set; } 64 | 65 | public string? SalesTaxBasis { get; set; } 66 | 67 | public string? SalesTaxPeriod { get; set; } 68 | 69 | public string? DefaultSalesTax { get; set; } 70 | 71 | public string? DefaultPurchasesTax { get; set; } 72 | 73 | public DateTime? PeriodLockDate { get; set; } 74 | 75 | public DateTime? EndOfYearLockDate { get; set; } 76 | 77 | public DateTime CreatedDateUtc { get; set; } 78 | 79 | public string? Timezone { get; set; } 80 | 81 | public string? ShortCode { get; set; } 82 | 83 | public string? Edition { get; set; } 84 | 85 | string ITenantRecord.RecordIdFieldName => nameof(OrganisationId); 86 | 87 | object ITenantRecord.RecordIdValue => OrganisationId; 88 | 89 | Guid ITenantRecord.RecordIdValue => OrganisationId; 90 | } 91 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ 'master' ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ 'master' ] 9 | schedule: 10 | - cron: '52 13 * * 1' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | language: [ 'csharp' ] 25 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 26 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 27 | 28 | steps: 29 | - name: 🥓 Setup .NET 30 | uses: actions/setup-dotnet@v4 31 | with: 32 | dotnet-version: 7.0.x 33 | - name: Checkout repository 34 | uses: actions/checkout@v4 35 | 36 | # Initializes the CodeQL tools for scanning. 37 | - name: Initialize CodeQL 38 | uses: github/codeql-action/init@v3 39 | with: 40 | languages: ${{ matrix.language }} 41 | # If you wish to specify custom queries, you can do so here or in a config file. 42 | # By default, queries listed here will override any specified in a config file. 43 | # Prefix the list here with "+" to use these queries and those in the config file. 44 | 45 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 46 | queries: +security-and-quality 47 | 48 | 49 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 50 | # If this step fails, then you should remove it and run the build manually (see below) 51 | - name: Autobuild 52 | uses: github/codeql-action/autobuild@v3 53 | 54 | # ℹ️ Command-line programs to run using the OS shell. 55 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 56 | 57 | # If the Autobuild fails above, remove it and uncomment the following three lines. 58 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 59 | 60 | # - run: | 61 | # echo "Run, Build Application using script" 62 | # ./location_of_script_within_repo/buildscript.sh 63 | 64 | - name: Perform CodeQL Analysis 65 | uses: github/codeql-action/analyze@v3 66 | with: 67 | category: "/language:${{matrix.language}}" 68 | -------------------------------------------------------------------------------- /src/Swan.Data/Data/Providers/SqliteColumn.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CA1812 2 | namespace Swan.Data.Providers; 3 | 4 | [ExcludeFromCodeCoverage] 5 | internal record SqliteColumn : IDbColumnSchema 6 | { 7 | public SqliteColumn() 8 | { 9 | // placeholder 10 | } 11 | 12 | public bool? AllowDBNull { get; set; } 13 | 14 | public string? BaseCatalogName { get; set; } 15 | 16 | public string? BaseColumnName { get; set; } 17 | 18 | public string? BaseSchemaName { get; set; } 19 | 20 | public string? BaseTableName { get; set; } 21 | 22 | public string? ColumnName { get; set; } 23 | 24 | public int? ColumnOrdinal { get; set; } 25 | 26 | public int? ColumnSize { get; set; } 27 | 28 | public Type? DataType { get; set; } 29 | 30 | public string? DataTypeName { get; set; } 31 | 32 | public string? ProviderType { get; set; } 33 | 34 | public bool? IsAliased { get; set; } 35 | 36 | public bool? IsAutoIncrement { get; set; } 37 | 38 | public bool? IsExpression { get; set; } 39 | 40 | public bool? IsKey { get; set; } 41 | 42 | public bool? IsUnique { get; set; } 43 | 44 | public bool? IsLong { get; set; } 45 | 46 | public short? NumericPrecision { get; set; } 47 | 48 | public short? NumericScale { get; set; } 49 | 50 | string IDbColumnSchema.ColumnName => ColumnName ?? string.Empty; 51 | 52 | int IDbColumnSchema.ColumnOrdinal => ColumnOrdinal.GetValueOrDefault(); 53 | 54 | Type IDbColumnSchema.DataType => DataType ?? typeof(string); 55 | 56 | string IDbColumnSchema.DataTypeName => DataTypeName ?? string.Empty; 57 | 58 | string IDbColumnSchema.ProviderType => ProviderType ?? string.Empty; 59 | 60 | bool IDbColumnSchema.AllowDBNull => AllowDBNull.GetValueOrDefault(); 61 | 62 | bool IDbColumnSchema.IsKey => IsKey.GetValueOrDefault(); 63 | 64 | bool IDbColumnSchema.IsAutoIncrement => IsAutoIncrement.GetValueOrDefault() || 65 | (IsKey.GetValueOrDefault() && (DataTypeName ?? string.Empty).ToUpperInvariant().Equals("INTEGER", StringComparison.Ordinal)); 66 | 67 | bool IDbColumnSchema.IsReadOnly => IsAutoIncrement.GetValueOrDefault(); 68 | 69 | int IDbColumnSchema.ColumnSize => ColumnSize.GetValueOrDefault(); 70 | 71 | int IDbColumnSchema.NumericPrecision => Convert.ToInt32(NumericPrecision.GetValueOrDefault().ClampMin(0)); 72 | 73 | int IDbColumnSchema.NumericScale => Convert.ToInt32(NumericScale.GetValueOrDefault().ClampMin(0)); 74 | 75 | bool IDbColumnSchema.IsLong => IsLong.GetValueOrDefault(); 76 | 77 | bool IDbColumnSchema.IsUnique => IsUnique.GetValueOrDefault(); 78 | 79 | object ICloneable.Clone() => this with { }; 80 | } 81 | 82 | #pragma warning restore CA1812 83 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /src/Swan.Data/Data/Providers/SqlServerDbProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Data.Providers; 2 | 3 | internal class SqlServerDbProvider : DbProvider 4 | { 5 | public override string DefaultSchemaName => "dbo"; 6 | 7 | public override IDbTypeMapper TypeMapper { get; } = new SqlServerTypeMapper(); 8 | 9 | public override Func ColumnSchemaFactory { get; } = () => new SqlServerColumn(); 10 | 11 | public override DbCommand CreateListTablesCommand(DbConnection connection) => 12 | connection is null 13 | ? throw new ArgumentNullException(nameof(connection)) 14 | : connection 15 | .BeginCommandText("SELECT [TABLE_NAME] AS [Name], [TABLE_SCHEMA] AS [Schema], IIF([TABLE_TYPE] = 'VIEW', 1, 0) AS [IsView] FROM [INFORMATION_SCHEMA].[TABLES]") 16 | .EndCommandText(); 17 | 18 | public override DbCommand CreateTableDdlCommand(DbConnection connection, IDbTableSchema table) 19 | { 20 | var (quotedTableName, orderedFields) = GetQuotedTableNameAndColumns(connection, table); 21 | var builder = new StringBuilder($"IF OBJECT_ID('{quotedTableName}') IS NULL\r\nCREATE TABLE {quotedTableName} (\r\n") 22 | .Append(string.Join(",\r\n", orderedFields.Select(c => $" {GetColumnDdlString(c)}").ToArray())) 23 | .AppendLine("\r\n);"); 24 | 25 | return connection 26 | .BeginCommandText(builder.ToString()) 27 | .EndCommandText(); 28 | } 29 | 30 | public override string? GetColumnDdlString(IDbColumnSchema column) => column is null 31 | ? throw new ArgumentNullException(nameof(column)) 32 | : !TypeMapper.TryGetProviderTypeFor(column, out var providerType) 33 | ? default 34 | : column.IsIdentity && column.DataType.TypeInfo().IsNumeric 35 | ? $"{QuoteField(column.ColumnName),16} {providerType} IDENTITY NOT NULL PRIMARY KEY" 36 | : base.GetColumnDdlString(column); 37 | 38 | public override bool TryGetSelectLastInserted(IDbTableSchema table, out string? commandText) 39 | { 40 | commandText = null; 41 | if (table.IdentityKeyColumn is null || table.KeyColumns.Count != 1) 42 | return false; 43 | 44 | var quotedFields = string.Join(", ", table.Columns.Select(c => QuoteField(c.ColumnName))); 45 | var quotedTable = QuoteTable(table.TableName, table.Schema); 46 | var quotedKeyField = QuoteField(table.IdentityKeyColumn.ColumnName); 47 | 48 | commandText = $"SELECT TOP 1 {quotedFields} FROM {quotedTable} WHERE {quotedKeyField} = SCOPE_IDENTITY()"; 49 | return true; 50 | } 51 | 52 | public override string GetLimitClause(int skip, int take) => 53 | $"OFFSET {skip} ROWS FETCH NEXT {take} ROWS ONLY"; 54 | } 55 | 56 | -------------------------------------------------------------------------------- /test/Swan.Test/Mocks/ProjectRecord.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test.Mocks; 2 | 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | 6 | /// 7 | /// Represents a record that maps to the dbo.Projects table. 8 | /// 9 | [Table("Projects", Schema = "dbo")] 10 | public record Project 11 | { 12 | /// 13 | /// Creates a new instance of the class. 14 | /// 15 | public Project() { /* placeholder */ } 16 | 17 | /// 18 | /// Gets or sets a value for Project Id. 19 | /// 20 | [Key] 21 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 22 | [Column(nameof(ProjectId), Order = 0)] 23 | public int ProjectId { get; set; } 24 | 25 | /// 26 | /// Gets or sets a value for Name. 27 | /// 28 | [MaxLength(100)] 29 | [Column(nameof(Name), Order = 1)] 30 | public string? Name { get; set; } 31 | 32 | /// 33 | /// Gets or sets a value for Project Type. 34 | /// 35 | [Column(nameof(ProjectType), Order = 2)] 36 | public ProjectTypes ProjectType { get; set; } 37 | 38 | /// 39 | /// Gets or sets a value for Company Id. 40 | /// 41 | [Column(nameof(CompanyId), Order = 3)] 42 | public int? CompanyId { get; set; } 43 | 44 | /// 45 | /// Gets or sets a value for Is Active. 46 | /// 47 | [Column(nameof(IsActive), Order = 4)] 48 | public bool IsActive { get; set; } 49 | 50 | /// 51 | /// Gets or sets a value for Start Date. 52 | /// 53 | [Column(nameof(StartDate), Order = 5)] 54 | public DateTime? StartDate { get; set; } 55 | 56 | /// 57 | /// Gets or sets a value for End Date. 58 | /// 59 | [Column(nameof(EndDate), Order = 6)] 60 | public DateTime? EndDate { get; set; } 61 | 62 | /// 63 | /// Gets or sets a value for Project Scope. 64 | /// 65 | [MaxLength(2147483647)] 66 | [Column(nameof(ProjectScope), Order = 7)] 67 | public string ProjectScope { get; set; } 68 | } 69 | 70 | [Table("ProjectNoColumns", Schema = "dbo")] 71 | public record ProjectNoColumns 72 | { 73 | /// 74 | /// Creates a new instance of the class. 75 | /// 76 | public ProjectNoColumns() { /* placeholder */ } 77 | 78 | /// 79 | /// Gets or sets a value for Project Id. 80 | /// 81 | [Key] 82 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 83 | [Column(nameof(ProjectId), Order = 0)] 84 | public int ProjectId { get; set; } 85 | } 86 | 87 | public enum ProjectTypes 88 | { 89 | Boring, 90 | Exciting 91 | } 92 | -------------------------------------------------------------------------------- /src/Swan.Artifacts/Extensions/ArtifactExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Extensions; 2 | 3 | using System.Globalization; 4 | 5 | /// 6 | /// Provides miscellaneous extension methods. 7 | /// 8 | public static class ArtifactExtensions 9 | { 10 | /// 11 | /// Gets the line and column number (i.e. not index) of the 12 | /// specified character index. Useful to locate text in a multi-line 13 | /// string the same way a text editor does. 14 | /// Please not that the tuple contains first the line number and then the 15 | /// column number. 16 | /// 17 | /// The string. 18 | /// Index of the character. 19 | /// A 2-tuple whose value is (item1, item2). 20 | public static Tuple TextPositionAt(this string? value, int charIndex) 21 | { 22 | if (value == null) 23 | return Tuple.Create(0, 0); 24 | 25 | var index = charIndex.Clamp(0, value.Length - 1); 26 | 27 | var lineIndex = 0; 28 | var colNumber = 0; 29 | 30 | for (var i = 0; i <= index; i++) 31 | { 32 | if (value[i] == '\n') 33 | { 34 | lineIndex++; 35 | colNumber = 0; 36 | continue; 37 | } 38 | 39 | if (value[i] != '\r') 40 | colNumber++; 41 | } 42 | 43 | return Tuple.Create(lineIndex + 1, colNumber); 44 | } 45 | 46 | /// 47 | /// Rounds up a date to match a timespan. 48 | /// 49 | /// The datetime. 50 | /// The timespan to match. 51 | /// 52 | /// A new instance of the DateTime structure to the specified datetime and timespan ticks. 53 | /// 54 | public static DateTime RoundUp(this DateTime date, TimeSpan timeSpan) 55 | => new(((date.Ticks + timeSpan.Ticks - 1) / timeSpan.Ticks) * timeSpan.Ticks); 56 | 57 | /// 58 | /// Converts a to the RFC1123 format. 59 | /// 60 | /// The on which this method is called. 61 | /// The string representation of according to RFC1123. 62 | /// 63 | /// If is not a UTC date / time, its UTC equivalent is converted, leaving unchanged. 64 | /// 65 | public static string ToRfc1123String(this DateTime @this) 66 | => @this.ToUniversalTime().ToString("R", CultureInfo.InvariantCulture); 67 | } 68 | -------------------------------------------------------------------------------- /src/Swan.Data/Data/Schema/IDbTableSchema.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Data.Schema; 2 | 3 | /// 4 | /// Represents table structure information from the backing data store. 5 | /// 6 | public interface IDbTableSchema 7 | { 8 | /// 9 | /// Gets the column schema data for the given column name. 10 | /// 11 | /// The column name. 12 | /// The column schema. 13 | IDbColumnSchema? this[string name] { get; } 14 | 15 | /// 16 | /// Gets the column schema data for the given column index. 17 | /// 18 | /// The 0-based index of the column. 19 | /// The column schema. 20 | IDbColumnSchema? this[int index] { get; } 21 | 22 | /// 23 | /// Gets the database name (catalog) this table belongs to. 24 | /// 25 | string Database { get; } 26 | 27 | /// 28 | /// Gets the schema name this table belongs to. Returns 29 | /// an empty string if provider does not support schemas. 30 | /// 31 | string Schema { get; } 32 | 33 | /// 34 | /// Gets the name of the table. 35 | /// 36 | string TableName { get; } 37 | 38 | /// 39 | /// Gets the number of columns. 40 | /// 41 | public int ColumnCount { get; } 42 | 43 | /// 44 | /// Gets the list of columns contained in this table. 45 | /// 46 | IReadOnlyList Columns { get; } 47 | 48 | /// 49 | /// Gets a list of key columns by qhich records are uniquely identified. 50 | /// 51 | IReadOnlyList KeyColumns { get; } 52 | 53 | /// 54 | /// Gets the identity column (if any) for this table. 55 | /// Returns null if no identity column is found. 56 | /// 57 | IDbColumnSchema? IdentityKeyColumn { get; } 58 | 59 | /// 60 | /// Determines if the table has an identity column. 61 | /// 62 | bool HasKeyIdentityColumn { get; } 63 | 64 | /// 65 | /// Gets the columns that can be used for Insert statements. 66 | /// These are columns that are not read-only or automatically set by the RDBMS. 67 | /// 68 | IReadOnlyList InsertableColumns { get; } 69 | 70 | /// 71 | /// Gets the columns that can be used for update statements. 72 | /// 73 | IReadOnlyList UpdateableColumns { get; } 74 | 75 | /// 76 | /// Gets the 0-based ordinal of the column in the order they were added. 77 | /// 78 | /// The name of the column. 79 | /// 80 | int GetColumnOrdinal(string columnName); 81 | } 82 | -------------------------------------------------------------------------------- /src/Swan.Net/Eventing.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Net; 2 | 3 | using System; 4 | using System.Text; 5 | 6 | /// 7 | /// The event arguments for connection failure events. 8 | /// 9 | /// 10 | public class ConnectionFailureEventArgs : EventArgs 11 | { 12 | /// 13 | /// Initializes a new instance of the class. 14 | /// 15 | /// The ex. 16 | public ConnectionFailureEventArgs(Exception ex) 17 | { 18 | Error = ex; 19 | } 20 | 21 | /// 22 | /// Gets the error. 23 | /// 24 | /// 25 | /// The error. 26 | /// 27 | public Exception Error { get; } 28 | } 29 | 30 | /// 31 | /// Event arguments for when data is received. 32 | /// 33 | /// 34 | public class ConnectionDataReceivedEventArgs : EventArgs 35 | { 36 | /// 37 | /// Initializes a new instance of the class. 38 | /// 39 | /// The buffer. 40 | /// The trigger. 41 | /// if set to true [more available]. 42 | public ConnectionDataReceivedEventArgs(byte[] buffer, ConnectionDataReceivedTrigger trigger, bool moreAvailable) 43 | { 44 | Buffer = buffer ?? throw new ArgumentNullException(nameof(buffer)); 45 | Trigger = trigger; 46 | HasMoreAvailable = moreAvailable; 47 | } 48 | 49 | /// 50 | /// Gets the buffer. 51 | /// 52 | /// 53 | /// The buffer. 54 | /// 55 | public byte[] Buffer { get; } 56 | 57 | /// 58 | /// Gets the cause as to why this event was thrown. 59 | /// 60 | /// 61 | /// The trigger. 62 | /// 63 | public ConnectionDataReceivedTrigger Trigger { get; } 64 | 65 | /// 66 | /// Gets a value indicating whether the receive buffer has more bytes available. 67 | /// 68 | /// 69 | /// true if this instance has more available; otherwise, false. 70 | /// 71 | public bool HasMoreAvailable { get; } 72 | 73 | /// 74 | /// Gets the string from buffer. 75 | /// 76 | /// The encoding. 77 | /// 78 | /// A that contains the results of decoding the specified sequence of bytes. 79 | /// 80 | /// encoding. 81 | public string GetStringFromBuffer(Encoding encoding) 82 | => encoding?.GetString(Buffer).TrimEnd('\r', '\n') ?? throw new ArgumentNullException(nameof(encoding)); 83 | } 84 | -------------------------------------------------------------------------------- /src/Swan.Core/Mapping/ObjectMap.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Mapping; 2 | 3 | using Reflection; 4 | 5 | /// 6 | /// Provides a basic implementation of a 7 | /// It's basically a dictionary of target properties to value providers. 8 | /// 9 | internal class ObjectMap : ConcurrentDictionary, IObjectMap 10 | { 11 | /// 12 | /// Creates a new instance of the class. 13 | /// It also populates a default map of properties that have the same names 14 | /// and compatible types. 15 | /// 16 | /// The parent object mapper containing all other maps. 17 | /// The source type for this object map. 18 | /// The target type for this object map. 19 | public ObjectMap(ObjectMapper context, ITypeInfo sourceType, ITypeInfo targetType) 20 | { 21 | Context = context ?? throw new ArgumentNullException(nameof(context)); 22 | TargetType = targetType ?? throw new ArgumentNullException(nameof(targetType)); 23 | SourceType = sourceType ?? throw new ArgumentNullException(nameof(sourceType)); 24 | 25 | foreach (var targetProperty in TargetType.Properties()) 26 | { 27 | if (!targetProperty.CanWrite) 28 | continue; 29 | 30 | if (!SourceType.TryFindProperty(targetProperty.PropertyName, out var sourceProperty)) 31 | continue; 32 | 33 | if (!sourceProperty.CanRead) 34 | continue; 35 | 36 | if (!targetProperty.IsAssignableFrom(sourceProperty)) 37 | continue; 38 | 39 | this[targetProperty] = (source) => sourceProperty.TryRead(source, out var value) 40 | ? value 41 | : targetProperty.DefaultValue; 42 | } 43 | } 44 | 45 | protected ObjectMapper Context { get; } 46 | 47 | /// 48 | public ITypeInfo TargetType { get; } 49 | 50 | /// 51 | public ITypeInfo SourceType { get; } 52 | 53 | /// 54 | public virtual object Apply(object source) => 55 | Apply(source, TargetType.CreateInstance()); 56 | 57 | /// 58 | public virtual object Apply(object source, object target) 59 | { 60 | if (target is null) 61 | throw new ArgumentNullException(nameof(target)); 62 | 63 | if (target.GetType() != TargetType.NativeType) 64 | throw new ArgumentException($"Parameter {nameof(target)} must be of type '{TargetType.FullName}'"); 65 | 66 | foreach (var (targetProperty, valueProvider) in this) 67 | { 68 | var sourceValue = valueProvider.Invoke(source); 69 | targetProperty.TryWrite(target, sourceValue); 70 | } 71 | 72 | return target; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Swan.Core/Gizmos/SlidingWindowRateLimiter.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Gizmos; 2 | 3 | /// 4 | /// Provides a simple, thread-safe mechanism to limit the amount of calls 5 | /// made to a subsequent statement. 6 | /// 7 | public class SlidingWindowRateLimiter 8 | { 9 | private readonly object SyncLock = new(); 10 | private readonly SortedList CallTimes; 11 | 12 | /// 13 | /// Creates an instance of the 14 | /// class with a sliding window of 60 seconds and a call limit of 60. 15 | /// 16 | public SlidingWindowRateLimiter() 17 | : this(TimeSpan.FromSeconds(60), 60) 18 | { 19 | // placheolder 20 | } 21 | 22 | /// 23 | /// Creates an instance of the class. 24 | /// 25 | /// The time window. Must be positive. 26 | /// The call limit. Must be a positive number. 27 | public SlidingWindowRateLimiter(TimeSpan timeWindow, int limit) 28 | { 29 | if (timeWindow.TotalMilliseconds <= 0) 30 | throw new ArgumentOutOfRangeException(nameof(timeWindow)); 31 | 32 | if (limit <= 0) 33 | throw new ArgumentOutOfRangeException(nameof(limit)); 34 | 35 | TimeWindow = timeWindow; 36 | Limit = limit; 37 | CallTimes = new(Limit + 1); 38 | } 39 | 40 | /// 41 | /// Gets the time window. 42 | /// 43 | public TimeSpan TimeWindow { get; } 44 | 45 | /// 46 | /// Gets the configured call limit. 47 | /// 48 | public int Limit { get; } 49 | 50 | /// 51 | /// Waits for a slot in the window to be available and 52 | /// returns when there is one. 53 | /// 54 | /// An awaitable task. 55 | public async Task WaitAsync(CancellationToken ct = default) 56 | { 57 | while (!ct.IsCancellationRequested) 58 | { 59 | lock (SyncLock) 60 | { 61 | var windowStartTime = DateTime.UtcNow.Subtract(TimeWindow); 62 | var sortedCallTimes = CallTimes.Keys.ToArray(); 63 | 64 | // get rid of times before window start time 65 | foreach (var callTime in sortedCallTimes) 66 | { 67 | // once we've reached the start time 68 | // stop removing items 69 | if (callTime >= windowStartTime) 70 | break; 71 | 72 | CallTimes.Remove(callTime); 73 | } 74 | 75 | if (CallTimes.Count < Limit) 76 | { 77 | CallTimes.Add(DateTime.UtcNow, default); 78 | return; 79 | } 80 | } 81 | 82 | await Task.Delay(1, ct).ConfigureAwait(false); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Swan.Data/Data/Providers/SqlServerColumn.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable CA1812 2 | namespace Swan.Data.Providers; 3 | 4 | [ExcludeFromCodeCoverage] 5 | internal record SqlServerColumn : IDbColumnSchema 6 | { 7 | public bool? AllowDBNull { get; set; } 8 | 9 | public string? BaseCatalogName { get; set; } 10 | 11 | public string? BaseColumnName { get; set; } 12 | 13 | public string? BaseSchemaName { get; set; } 14 | 15 | public string? BaseServerName { get; set; } 16 | 17 | public string? BaseTableName { get; set; } 18 | 19 | public string? ColumnName { get; set; } 20 | 21 | public int? ColumnOrdinal { get; set; } 22 | 23 | public int? ColumnSize { get; set; } 24 | 25 | public Type? DataType { get; set; } 26 | 27 | public string? DataTypeName { get; set; } 28 | 29 | public int? IsAliased { get; set; } 30 | 31 | public bool? IsAutoIncrement { get; set; } 32 | 33 | public bool? IsColumnSet { get; set; } 34 | 35 | public bool? IsExpression { get; set; } 36 | 37 | public bool? IsHidden { get; set; } 38 | 39 | public bool? IsIdentity { get; set; } 40 | 41 | public bool? IsKey { get; set; } 42 | 43 | public bool? IsLong { get; set; } 44 | 45 | public bool? IsReadOnly { get; set; } 46 | 47 | public bool? IsRowVersion { get; set; } 48 | 49 | public bool? IsUnique { get; set; } 50 | 51 | public SqlDbType NonVersionedProviderType { get; set; } 52 | 53 | public int? NumericPrecision { get; set; } 54 | 55 | public int? NumericScale { get; set; } 56 | 57 | public string? ProviderSpecificDataType { get; set; } 58 | 59 | public SqlDbType? ProviderType { get; set; } 60 | 61 | public string? UdtAssemblyQualifiedName { get; set; } 62 | 63 | int IDbColumnSchema.NumericPrecision => NumericPrecision.GetValueOrDefault(byte.MaxValue) == byte.MaxValue ? -1 : NumericPrecision ?? -1; 64 | 65 | int IDbColumnSchema.NumericScale => NumericScale.GetValueOrDefault(byte.MaxValue) == byte.MaxValue ? -1 : NumericScale ?? -1; 66 | 67 | int IDbColumnSchema.ColumnSize => ColumnSize ?? -1; 68 | 69 | string IDbColumnSchema.ColumnName => ColumnName ?? string.Empty; 70 | 71 | int IDbColumnSchema.ColumnOrdinal => ColumnOrdinal.GetValueOrDefault(-1); 72 | 73 | Type IDbColumnSchema.DataType => DataType ?? typeof(string); 74 | 75 | string IDbColumnSchema.DataTypeName => DataTypeName ?? string.Empty; 76 | 77 | string IDbColumnSchema.ProviderType => ProviderType.GetValueOrDefault(SqlDbType.NVarChar).ToStringInvariant(); 78 | 79 | bool IDbColumnSchema.AllowDBNull => AllowDBNull.GetValueOrDefault(); 80 | 81 | bool IDbColumnSchema.IsKey => IsKey.GetValueOrDefault(); 82 | 83 | bool IDbColumnSchema.IsAutoIncrement => IsAutoIncrement.GetValueOrDefault(); 84 | 85 | bool IDbColumnSchema.IsReadOnly => IsReadOnly.GetValueOrDefault(); 86 | 87 | bool IDbColumnSchema.IsLong => IsLong.GetValueOrDefault(); 88 | 89 | bool IDbColumnSchema.IsUnique => IsUnique.GetValueOrDefault(); 90 | 91 | object ICloneable.Clone() => this with { }; 92 | } 93 | 94 | #pragma warning restore CA1812 95 | -------------------------------------------------------------------------------- /src/Swan.Data/Data/DbProviders.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Data; 2 | 3 | /// 4 | /// Provides a repository of data providers. 5 | /// 6 | public static class DbProviders 7 | { 8 | private static readonly Dictionary _cache = new(16, StringComparer.Ordinal); 9 | 10 | /// 11 | /// Initializes the static members of the class. 12 | /// 13 | static DbProviders() 14 | { 15 | _cache["System.Data.SqlClient"] = new SqlServerDbProvider(); 16 | _cache["Microsoft.Data.SqlClient"] = new SqlServerDbProvider(); 17 | _cache["MySql.Data.MySqlClient"] = new MySqlDbProvider(); 18 | _cache["Microsoft.Data.Sqlite"] = new SqliteDbProvider(); 19 | } 20 | 21 | /// 22 | /// Tries to obtain a registered provider for the connection type. 23 | /// 24 | /// The connection type. 25 | /// The resulting provider. 26 | /// True of the provider was previously registered and was retrieved. False otherwise. 27 | public static bool TryGetProvider(Type? connectionType, [MaybeNullWhen(false)] out DbProvider provider) 28 | { 29 | provider = null; 30 | 31 | if (connectionType is null) 32 | return false; 33 | 34 | if (connectionType.TypeInfo().Interfaces.All(c => c != typeof(IDbConnection))) 35 | return false; 36 | 37 | var providerNs = connectionType.Namespace ?? string.Empty; 38 | return _cache.TryGetValue(providerNs, out provider); 39 | } 40 | 41 | /// 42 | /// Tries to obtain a registered provider for the connection type. 43 | /// 44 | /// The connection type. 45 | /// The connection. 46 | /// The resulting provider. 47 | /// True of the provider was previously registered and was retrieved. False otherwise. 48 | public static bool TryGetProvider(this T? connection, [MaybeNullWhen(false)] out DbProvider provider) 49 | where T : IDbConnection 50 | { 51 | provider = null; 52 | return connection is not null && TryGetProvider(connection.GetType(), out provider); 53 | } 54 | 55 | /// 56 | /// Adds or updates a provider registration for the given connection type. 57 | /// 58 | /// The connection type. 59 | /// The provider instance to register. 60 | public static void RegisterProvider(DbProvider provider) 61 | where T : class, IDbConnection 62 | { 63 | if (provider is null) 64 | throw new ArgumentNullException(nameof(provider)); 65 | 66 | var providerNs = typeof(T).Namespace; 67 | if (string.IsNullOrWhiteSpace(providerNs)) 68 | throw new ArgumentException($"No namespace found for type {typeof(T)}", nameof(T)); 69 | 70 | _cache[providerNs] = provider; 71 | } 72 | } 73 | 74 | -------------------------------------------------------------------------------- /src/Swan.Core/Reflection/IPropertyProxy.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Reflection; 2 | 3 | /// 4 | /// Represents a generic interface to store getters and setters for high speed access to properties. 5 | /// 6 | public interface IPropertyProxy 7 | { 8 | /// 9 | /// Gets the type proxy that owns this property proxy. 10 | /// 11 | ITypeInfo ParentType { get; } 12 | 13 | /// 14 | /// Gets the name of the property. 15 | /// 16 | string PropertyName { get; } 17 | 18 | /// 19 | /// Gets the associated reflection property info. 20 | /// 21 | PropertyInfo PropertyInfo { get; } 22 | 23 | /// 24 | /// Gets the property type metadata. 25 | /// 26 | ITypeInfo PropertyType { get; } 27 | 28 | /// 29 | /// Gets the default value for this type. 30 | /// Reference types return null while value types return their default equivalent. 31 | /// 32 | object? DefaultValue { get; } 33 | 34 | /// 35 | /// Gets all the custom attributes applied to this property declaration. 36 | /// 37 | IReadOnlyList PropertyAttributes { get; } 38 | 39 | /// 40 | /// Gets whether the property getter is declared as public. 41 | /// 42 | bool HasPublicGetter { get; } 43 | 44 | /// 45 | /// Gets whether the property setter is declared as public. 46 | /// 47 | bool HasPublicSetter { get; } 48 | 49 | /// 50 | /// Gets a value indicating whether this property can be read from. 51 | /// 52 | public bool CanRead { get; } 53 | 54 | /// 55 | /// Gets a value indicating whether this property can be written to. 56 | /// 57 | public bool CanWrite { get; } 58 | 59 | /// 60 | /// Gets the property value via a stored delegate. 61 | /// 62 | /// The instance. 63 | /// The property value. 64 | object? Read(object instance); 65 | 66 | /// 67 | /// Tries to read the current property from the given object. 68 | /// 69 | /// The target instance to read the property from. 70 | /// The output value. Will be set to the default value of the property type if unsuccessful. 71 | /// True if the operation succeeds. False otherwise. 72 | bool TryRead(object instance, out object? value); 73 | 74 | /// 75 | /// Sets the property value via a stored delegate. 76 | /// 77 | /// The instance. 78 | /// The value. 79 | void Write(object instance, object? value); 80 | 81 | /// 82 | /// Tries to safely set the value of the property by changing types if necessary. 83 | /// 84 | /// The target instance containing the property. 85 | /// The value to pass into the property. 86 | /// Returns true if the operation was successful. False otherwise. 87 | bool TryWrite(object instance, object? value); 88 | } 89 | -------------------------------------------------------------------------------- /test/Swan.Test/CsvWriterGenericTest.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Test; 2 | 3 | using Formatters; 4 | using Mocks; 5 | 6 | [TestFixture] 7 | public class CsvWriterGenericTest 8 | { 9 | private readonly Func ValueProvider = str => str.ToUpper(); 10 | 11 | [Test] 12 | public void WithNullValuesGiven_ThrowsException() 13 | { 14 | var stream = new MemoryStream(); 15 | var writer = new CsvWriter(stream); 16 | 17 | Assert.Throws(() => writer.WriteLine(null)); 18 | Assert.Throws(() => writer.AddMapping(null, ValueProvider)); 19 | Assert.Throws(() => writer.RemoveMapping(null)); 20 | } 21 | 22 | [Test] 23 | public void AddMapingWhenHasWrittenHeadingsTrue_ThrowsExepction() 24 | { 25 | var objHeaders = new SampleCsvRecord(); 26 | 27 | using var stream = new MemoryStream(); 28 | using var writer = new CsvWriter(stream, separatorChar: '#'); 29 | 30 | writer.WriteLine(objHeaders); 31 | writer.Flush(); 32 | 33 | Assert.Throws(() => writer.AddMapping("HeaderName", ValueProvider)); 34 | } 35 | 36 | [Test] 37 | public void ClearMapingsWhenHasWrittenHeadingsTrue_ThrowsExepction() 38 | { 39 | var objHeaders = new SampleCsvRecord(); 40 | 41 | using var stream = new MemoryStream(); 42 | using var writer = new CsvWriter(stream, separatorChar: '#'); 43 | 44 | writer.WriteLine(objHeaders); 45 | writer.Flush(); 46 | 47 | Assert.Throws(() => writer.ClearMappings()); 48 | } 49 | 50 | [Test] 51 | public void RemoveMapingsWhenHasWrittenHeadingsTrue_ThrowsExepction() 52 | { 53 | var objHeaders = new SampleCsvRecord(); 54 | 55 | using var stream = new MemoryStream(); 56 | using var writer = new CsvWriter(stream, separatorChar: '#'); 57 | 58 | writer.WriteLine(objHeaders); 59 | writer.Flush(); 60 | 61 | Assert.Throws(() => writer.RemoveMapping("HeadingName")); 62 | } 63 | 64 | [Test] 65 | public void WithCSVWriter_addsMapping() 66 | { 67 | using var stream = new MemoryStream(); 68 | using var writer = new CsvWriter(stream); 69 | 70 | writer.AddMapping("HeadingName", ValueProvider); 71 | 72 | Assert.IsNotNull(writer.PropertyMap.ContainsKey("HeadingName")); 73 | } 74 | 75 | [Test] 76 | public void WithCSVWriter_addsAndThenClearsMappings() 77 | { 78 | using var stream = new MemoryStream(); 79 | using var writer = new CsvWriter(stream); 80 | 81 | writer.AddMapping("HeadingName", ValueProvider); 82 | writer.ClearMappings(); 83 | 84 | Assert.AreEqual(0, writer.PropertyMap.Count); 85 | } 86 | 87 | [Test] 88 | public void WithCSVWriter_addsAndThenRemovesMappingByName() 89 | { 90 | using var stream = new MemoryStream(); 91 | using var writer = new CsvWriter(stream); 92 | 93 | writer.AddMapping("HeadingName", ValueProvider); 94 | writer.RemoveMapping("HeadingName"); 95 | 96 | Assert.AreEqual(0, writer.PropertyMap.Count); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /.github/workflows/lib-ci.yml: -------------------------------------------------------------------------------- 1 | name: Lib - Build, Bump and Push (GitHub Registry) 2 | on: 3 | workflow_call: 4 | secrets: 5 | github-token: 6 | description: 'The GitHub PAT token to perform checkout and publish.' 7 | required: true 8 | inputs: 9 | project-name: 10 | description: 'The name of the project to build. Must match the directory name.' 11 | required: true 12 | type: string 13 | base-path: 14 | description: 'The working directory base path with a trailing forward-slash' 15 | required: false 16 | default: '/home/runner/work/${{ github.event.repository.name }}/${{ github.event.repository.name }}/' 17 | type: string 18 | source-path: 19 | description: > 20 | The source path relative to the base path with a trailing forward-slash. 21 | For example, if the project folder is in a subfolder called src, then this value needs to be set to src/ 22 | If there is no subfolder, just leave it empty. 23 | required: false 24 | default: '' 25 | type: string 26 | prerelease-versions-kept: 27 | description: 'The number of pre-release versions kept in the GitHub registry.' 28 | required: false 29 | type: number 30 | default: 10 31 | version-bump-level: 32 | description: 'The version component to bump after publishing.' 33 | required: false 34 | default: ${{ contains(github.event.head_commit.message, '[PATCH]') && 'patch' || contains(github.event.head_commit.message, '[MINOR]') && 'minor' || contains(github.event.head_commit.message, '[MAJOR]') && 'major' || 'prerelease' }} 35 | type: string 36 | jobs: 37 | lib-ci: 38 | runs-on: ubuntu-latest 39 | steps: 40 | - name: 🥓 Setup .NET 41 | uses: actions/setup-dotnet@v4 42 | with: 43 | dotnet-version: | 44 | 5.0.x 45 | 6.0.x 46 | 7.0.x 47 | 8.0.x 48 | - name: 🧑‍💻 Checkout codebase 49 | uses: actions/checkout@v4 50 | with: 51 | token: ${{ secrets.github-token }} 52 | ref: ${{ github.ref_name }} 53 | - name: 🐝 Bump version 54 | uses: unosquare/bump-nuget-version@master 55 | with: 56 | level: ${{ inputs.version-bump-level }} 57 | csproj: '${{ inputs.base-path }}${{ inputs.source-path }}${{ inputs.project-name }}/${{ inputs.project-name }}.csproj' 58 | - name: 🏗 Build and pack nuget 59 | run: | 60 | dotnet pack '${{ inputs.base-path }}${{ inputs.source-path }}${{ inputs.project-name }}/${{ inputs.project-name }}.csproj' --configuration Release 61 | - name: 📌 Commit 62 | uses: EndBug/add-and-commit@v9 63 | with: 64 | message: '[skip ci]' 65 | - name: 🍱 Publish nuget 66 | uses: unosquare/publish-nuget-package@master 67 | with: 68 | access-token: ${{ secrets.github-token }} 69 | path: '${{ inputs.base-path }}${{ inputs.source-path }}${{ inputs.project-name }}/bin/Release' 70 | - name: 🌿 Clean up stale packages 71 | uses: actions/delete-package-versions@v5 72 | with: 73 | package-name: ${{ inputs.project-name }} 74 | package-type: 'nuget' 75 | min-versions-to-keep: ${{ inputs.prerelease-versions-kept }} 76 | delete-only-pre-release-versions: true 77 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at geovanni.perez@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /src/Swan.Core/Gizmos/Paginator.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Gizmos; 2 | using Extensions; 3 | 4 | /// 5 | /// A utility class to compute paging or batching offsets. 6 | /// 7 | public class Paginator 8 | { 9 | /// 10 | /// Initializes a new instance of the class. 11 | /// 12 | /// The total count of items to page over. Value must be a non-negative number. 13 | /// The desired size of individual pages. Value must be 1 or greater. 14 | public Paginator(int totalCount, int pageSize) 15 | { 16 | TotalCount = totalCount.ClampMin(0); 17 | PageSize = pageSize.ClampMin(1); 18 | PageCount = ComputePageCount(); 19 | } 20 | 21 | /// 22 | /// Gets the desired number of items per page. 23 | /// 24 | public int PageSize { get; } 25 | 26 | /// 27 | /// Gets the total number of items to page over. 28 | /// 29 | public int TotalCount { get; } 30 | 31 | /// 32 | /// Gets the computed number of pages. 33 | /// 34 | public int PageCount { get; } 35 | 36 | /// 37 | /// Gets the start item index of the given page. 38 | /// 39 | /// Zero-based index of the page. 40 | /// The start item index. 41 | public int GetFirstItemIndex(int pageIndex) => FixPageIndex(pageIndex) * PageSize; 42 | 43 | /// 44 | /// Gets the end item index of the given page. 45 | /// 46 | /// Zero-based index of the page. 47 | /// The end item index. 48 | public int GetLastItemIndex(int pageIndex) 49 | { 50 | var startIndex = GetFirstItemIndex(pageIndex); 51 | return Math.Min(startIndex + PageSize - 1, TotalCount - 1); 52 | } 53 | 54 | /// 55 | /// Gets the item count of the given page index. 56 | /// 57 | /// Zero-based index of the page. 58 | /// The number of items that the page contains. 59 | public int GetItemCount(int pageIndex) 60 | { 61 | pageIndex = FixPageIndex(pageIndex); 62 | return (pageIndex >= PageCount - 1) 63 | ? GetLastItemIndex(pageIndex) - GetFirstItemIndex(pageIndex) + 1 64 | : PageSize; 65 | } 66 | 67 | /// 68 | /// Fixes the index of the page by applying bound logic. 69 | /// 70 | /// Index of the page. 71 | /// A limit-bound index. 72 | private int FixPageIndex(int pageIndex) => pageIndex < 0 73 | ? 0 74 | : pageIndex >= PageCount 75 | ? PageCount - 1 76 | : pageIndex; 77 | 78 | /// 79 | /// Computes the number of pages for the paginator. 80 | /// 81 | /// The page count. 82 | private int ComputePageCount() 83 | { 84 | // include this if when you always want at least 1 page 85 | if (TotalCount == 0) 86 | return 0; 87 | 88 | return TotalCount % PageSize != 0 89 | ? (TotalCount / PageSize) + 1 90 | : TotalCount / PageSize; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Swan.Core/Gizmos/RangeExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Gizmos; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | internal static class RangeExtensions 8 | { 9 | /// 10 | /// Given a set of values representing a discretized continuum, this method 11 | /// maps the provided search value to the closest point that is equal or less than 12 | /// such value. Please note that the values are first sorted in ascending order. 13 | /// For example, given 100, 110 and 120: 14 | /// searching for 90 would yield 100, 15 | /// searching for 105 would yield 100, 16 | /// searching for 110 would yield 110, 17 | /// searching for 119 would yield 110, 18 | /// searching for 200 would yield 120 19 | /// 20 | /// The type of the values to search for. 21 | /// The values representing the discretized continuum. 22 | /// The value to be searched. 23 | /// A discrete value to which the search value belongs. 24 | public static T? FindStartValue(this IEnumerable? allValues, T searchValue) 25 | where T : IComparable, IComparable 26 | { 27 | if (allValues is null) 28 | return default; 29 | 30 | var sortedValues = allValues.ToArray(); 31 | if (!sortedValues.Any()) 32 | return default; 33 | 34 | Array.Sort(sortedValues); 35 | 36 | var firstValue = sortedValues[0]; 37 | if (searchValue.CompareTo(firstValue) < 0) 38 | return firstValue; 39 | 40 | var currentValue = firstValue; 41 | foreach (var value in sortedValues) 42 | { 43 | if (searchValue.CompareTo(value) >= 0) 44 | { 45 | currentValue = value; 46 | continue; 47 | } 48 | 49 | break; 50 | } 51 | 52 | return currentValue; 53 | } 54 | 55 | /// 56 | /// Provides a continuum mapping method similar to 57 | /// but operates on a . 58 | /// 59 | /// The type of keys to be searched. 60 | /// The type of the values to return. 61 | /// The sorted list to be searched by key. 62 | /// The key to be searched. 63 | /// The value of the matching point in the continuum. 64 | public static TValue? FindStartValue(this SortedList? values, TKey searchKey) 65 | where TKey : struct, IComparable, IComparable 66 | { 67 | if (values is null || values.Count <= 0) 68 | return default; 69 | 70 | var (firstKey, firstValue) = values.First(); 71 | if (searchKey.CompareTo(firstKey) < 0) 72 | return firstValue; 73 | 74 | var currentValue = firstValue; 75 | foreach (var (key, value) in values) 76 | { 77 | if (searchKey.CompareTo(key) >= 0) 78 | { 79 | currentValue = value; 80 | continue; 81 | } 82 | 83 | break; 84 | } 85 | 86 | return currentValue; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Swan.Core/Gizmos/RangeLookup.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Gizmos; 2 | 3 | /// 4 | /// Represents a dictionary of discrete keys 5 | /// that form a continuum to which keys can be mapped. 6 | /// 7 | /// The key type. Typically dates or integers representing years. 8 | /// The type of values stored within the keys. 9 | public class RangeLookup : IDictionary 10 | where TKey : struct, IComparable, IComparable 11 | { 12 | private readonly SortedList _values = new(); 13 | 14 | /// 15 | /// Creates a new instance of the class. 16 | /// 17 | public RangeLookup() 18 | { 19 | // placeholder 20 | } 21 | 22 | /// 23 | /// Creates a new instance of the class. 24 | /// 25 | /// The initial dictionary containing the data. 26 | public RangeLookup(IDictionary dictionary) 27 | { 28 | _values = new(dictionary); 29 | } 30 | 31 | /// 32 | public TValue this[TKey key] 33 | { 34 | get => _values.FindStartValue(key)!; 35 | set => _values[key] = value!; 36 | } 37 | 38 | /// 39 | public ICollection Keys => _values.Keys; 40 | 41 | /// 42 | public ICollection Values => _values.Values; 43 | 44 | /// 45 | public int Count => _values.Count; 46 | 47 | /// 48 | public bool IsReadOnly => false; 49 | 50 | /// 51 | public void Add(TKey key, TValue value) => this[key] = value; 52 | 53 | /// 54 | public void Add(KeyValuePair item) => _values[item.Key] = item.Value; 55 | 56 | /// 57 | public void Clear() => _values.Clear(); 58 | 59 | /// 60 | public bool Contains(KeyValuePair item) => _values.Contains(item); 61 | 62 | /// 63 | public bool ContainsKey(TKey key) => _values.ContainsKey(key); 64 | 65 | /// 66 | public void CopyTo(KeyValuePair[] array, int arrayIndex) 67 | { 68 | if (array is null) 69 | throw new ArgumentNullException(nameof(array)); 70 | 71 | var index = 0; 72 | foreach (var kvp in _values) 73 | { 74 | if (index >= arrayIndex) 75 | array[index] = kvp; 76 | 77 | ++index; 78 | } 79 | } 80 | 81 | /// 82 | public IEnumerator> GetEnumerator() => _values.GetEnumerator(); 83 | 84 | /// 85 | public bool Remove(TKey key) => _values.Remove(key); 86 | 87 | /// 88 | public bool Remove(KeyValuePair item) => _values.Remove(item.Key); 89 | 90 | /// 91 | public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) 92 | { 93 | value = this[key]; 94 | return true; 95 | } 96 | 97 | /// 98 | IEnumerator IEnumerable.GetEnumerator() => (_values as IEnumerable).GetEnumerator(); 99 | } 100 | -------------------------------------------------------------------------------- /src/Swan.Core/Reflection/TypeManager.Properties.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Reflection; 2 | 3 | public static partial class TypeManager 4 | { 5 | /// 6 | /// Converts a PropertyInfo object to an IPropertyProxy. 7 | /// 8 | /// The source property info. 9 | /// A corresponding property proxy. 10 | public static IPropertyProxy ToPropertyProxy(this PropertyInfo propertyInfo) 11 | { 12 | if (propertyInfo is null) 13 | throw new ArgumentNullException(nameof(propertyInfo)); 14 | 15 | if (propertyInfo.ReflectedType is null) 16 | throw new ArgumentException($"Unable to obtain enclosing type for property '{propertyInfo.Name}'.", nameof(propertyInfo)); 17 | 18 | return propertyInfo.ReflectedType.Property(propertyInfo.Name) 19 | ?? throw new KeyNotFoundException($"Could not find property '{propertyInfo.Name}' for type '{propertyInfo.ReflectedType.Name}'"); 20 | } 21 | 22 | /// 23 | /// Gets the property proxies associated with a given type. 24 | /// 25 | /// The type to retrieve property proxies from. 26 | /// The property proxies for the given type. 27 | public static IReadOnlyList Properties(this Type t) => t is not null 28 | ? t.TypeInfo().Properties.Values.ToArray() 29 | : throw new ArgumentNullException(nameof(t)); 30 | 31 | /// 32 | /// Gets the property proxies associated with a given type. 33 | /// 34 | /// The type to retrieve property proxies from. 35 | /// The property proxies for the given type. 36 | public static IReadOnlyList Properties(this ITypeInfo t) => t is not null 37 | ? t.Properties.Values.ToArray() 38 | : throw new ArgumentNullException(nameof(t)); 39 | 40 | /// 41 | /// Gets the property proxy given the property name. 42 | /// If the property is not found, it returns a null property proxy. 43 | /// 44 | /// The associated type. 45 | /// Name of the property. 46 | /// The associated if found; otherwise returns null. 47 | public static IPropertyProxy? Property(this ITypeInfo t, string propertyName) 48 | { 49 | if (t == null) 50 | throw new ArgumentNullException(nameof(t)); 51 | 52 | if (string.IsNullOrWhiteSpace(propertyName)) 53 | return null; 54 | 55 | return t.TryFindProperty(propertyName, out var property) 56 | ? property 57 | : null; 58 | } 59 | 60 | /// 61 | /// Gets the property proxy given the property name. 62 | /// If the property is not found, it returns a null property proxy. 63 | /// 64 | /// The associated type. 65 | /// Name of the property. 66 | /// The associated if found; otherwise returns null. 67 | public static IPropertyProxy? Property(this Type t, string propertyName) => 68 | t is not null 69 | ? t.TypeInfo().Property(propertyName) 70 | : throw new ArgumentNullException(nameof(t)); 71 | } 72 | -------------------------------------------------------------------------------- /src/Swan.Core/Reflection/TypeManager.Collections.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Reflection; 2 | 3 | using Collections; 4 | 5 | public static partial class TypeManager 6 | { 7 | /// 8 | /// Wraps any enumerable as a . 9 | /// 10 | /// The collection to wrap. 11 | /// A collection proxy that wraps the specified target. 12 | public static CollectionProxy AsProxy(this IEnumerable target) 13 | { 14 | if (target is null) 15 | throw new ArgumentNullException(nameof(target)); 16 | 17 | if (!CollectionProxy.TryCreate(target, out var proxy)) 18 | throw new ArgumentException("Unable to create collection proxy.", nameof(target)); 19 | 20 | return proxy; 21 | } 22 | 23 | /// 24 | /// Creates an instance of a from the given 25 | /// element type and count. 26 | /// 27 | /// The type of array elements. 28 | /// The array length. 29 | /// An instance of an array. 30 | public static CollectionProxy CreateArray(Type elementType, int elementCount) => 31 | elementType is null 32 | ? throw new ArgumentNullException(nameof(elementType)) 33 | : elementCount < 0 34 | ? throw new ArgumentOutOfRangeException(nameof(elementCount)) 35 | : Array.CreateInstance(elementType, elementCount).AsProxy(); 36 | 37 | /// 38 | /// Creates an instance of a from the given 39 | /// element type and count. 40 | /// 41 | /// The type of array elements. 42 | /// The array length. 43 | /// An instance of an array. 44 | public static CollectionProxy CreateArray(ITypeInfo elementType, int elementCount) => 45 | elementType is null 46 | ? throw new ArgumentNullException(nameof(elementType)) 47 | : elementCount < 0 48 | ? throw new ArgumentOutOfRangeException(nameof(elementCount)) 49 | : Array.CreateInstance(elementType.NativeType, elementCount).AsProxy(); 50 | 51 | /// 52 | /// Creates a from type arguments. 53 | /// 54 | /// The generic type argument. 55 | /// An instance of a 56 | public static CollectionProxy CreateGenericList(Type elementType) 57 | { 58 | var resultType = typeof(List<>).MakeGenericType(elementType); 59 | return (resultType.TypeInfo().CreateInstance() as IEnumerable)!.AsProxy(); 60 | } 61 | 62 | /// 63 | /// Creates an from type arguments. 64 | /// 65 | /// The type for keys. 66 | /// The type for values. 67 | /// An instance of a 68 | public static CollectionProxy CreateGenericDictionary(Type keyType, Type valueType) 69 | { 70 | var resultType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType); 71 | return (resultType.TypeInfo().CreateInstance() as IEnumerable)!.AsProxy(); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/Swan.Core/Platform/SwanRuntime.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Platform; 2 | 3 | using System.IO; 4 | using System.Threading; 5 | 6 | /// 7 | /// Provides utility methods to retrieve information about the current application. 8 | /// 9 | public static class SwanRuntime 10 | { 11 | private static readonly Lazy EntryAssemblyLazy = new(() => Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()); 12 | 13 | private static readonly string ApplicationMutexName = $"Global\\{{{{{EntryAssembly.FullName}}}}}"; 14 | 15 | private static readonly Lazy Windows1252EncodingLazy = new( 16 | Encoding.GetEncodings().FirstOrDefault(c => c.CodePage == 1252)?.GetEncoding() ?? Encoding.GetEncoding(default(int))); 17 | 18 | private static readonly object SyncLock = new(); 19 | 20 | /// 21 | /// Gets the Windows 1253 Encoding (if available). Otherwise returns the default ANSI encoding. 22 | /// 23 | public static Encoding Windows1252Encoding => Windows1252EncodingLazy.Value; 24 | 25 | /// 26 | /// Checks if this application (including version number) is the only instance currently running. 27 | /// 28 | /// 29 | /// true if this instance is the only instance; otherwise, false. 30 | /// 31 | public static bool IsTheOnlyInstance 32 | { 33 | get 34 | { 35 | lock (SyncLock) 36 | { 37 | try 38 | { 39 | // Try to open existing mutex. 40 | using var _ = Mutex.OpenExisting(ApplicationMutexName); 41 | } 42 | #pragma warning disable CA1031 // Do not catch general exception types 43 | catch 44 | #pragma warning restore CA1031 // Do not catch general exception types 45 | { 46 | try 47 | { 48 | // If exception occurred, there is no such mutex. 49 | using var appMutex = new Mutex(true, ApplicationMutexName); 50 | 51 | // Only one instance. 52 | return true; 53 | } 54 | #pragma warning disable CA1031 // Do not catch general exception types 55 | catch 56 | #pragma warning restore CA1031 // Do not catch general exception types 57 | { 58 | // Sometimes the user can't create the Global Mutex 59 | } 60 | } 61 | 62 | // More than one instance. 63 | return false; 64 | } 65 | } 66 | } 67 | 68 | /// 69 | /// Gets the assembly that started the application. 70 | /// 71 | /// 72 | /// The entry assembly. 73 | /// 74 | public static Assembly EntryAssembly => EntryAssemblyLazy.Value; 75 | 76 | /// 77 | /// Gets the full path to the folder containing the assembly that started the application. 78 | /// 79 | /// 80 | /// The entry assembly directory. 81 | /// 82 | public static string EntryAssemblyDirectory 83 | { 84 | get 85 | { 86 | var uri = new UriBuilder(EntryAssembly.Location); 87 | var path = Uri.UnescapeDataString(uri.Path); 88 | return Path.GetDirectoryName(path) ?? string.Empty; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Swan.Artifacts/Threading/PeriodicTask.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Threading; 2 | 3 | using System; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | /// 8 | /// Schedule an action to be periodically executed on the thread pool. 9 | /// 10 | public sealed class PeriodicTask : IDisposable 11 | { 12 | /// 13 | /// The minimum interval between action invocations. 14 | /// The value of this field is equal to 100 milliseconds. 15 | /// 16 | public static readonly TimeSpan MinInterval = TimeSpan.FromMilliseconds(100); 17 | 18 | private readonly Func _action; 19 | private readonly CancellationTokenSource _cancellationTokenSource; 20 | 21 | private TimeSpan _interval; 22 | 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | /// The interval between invocations of . 27 | /// The callback to invoke periodically. 28 | /// A that can be used to cancel operations. 29 | public PeriodicTask(TimeSpan interval, Func action, CancellationToken cancellationToken = default) 30 | { 31 | _action = action ?? throw new ArgumentNullException(nameof(action)); 32 | _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); 33 | _interval = ValidateInterval(interval); 34 | 35 | _ = Task.Run(ActionLoop, CancellationToken.None); 36 | } 37 | 38 | /// 39 | /// Finalizes an instance of the class. 40 | /// 41 | ~PeriodicTask() 42 | { 43 | Dispose(false); 44 | } 45 | 46 | /// 47 | /// Gets or sets the interval between periodic action invocations. 48 | /// Changes to this property take effect after next action invocation. 49 | /// 50 | /// 51 | public TimeSpan Interval 52 | { 53 | get => _interval; 54 | set => _interval = ValidateInterval(value); 55 | } 56 | 57 | /// 58 | public void Dispose() 59 | { 60 | Dispose(true); 61 | GC.SuppressFinalize(this); 62 | } 63 | 64 | private void Dispose(bool disposing) 65 | { 66 | if (disposing) 67 | { 68 | _cancellationTokenSource.Cancel(); 69 | _cancellationTokenSource.Dispose(); 70 | } 71 | } 72 | 73 | private static TimeSpan ValidateInterval(TimeSpan value) 74 | => value < MinInterval ? MinInterval : value; 75 | 76 | private async Task ActionLoop() 77 | { 78 | for (; ; ) 79 | { 80 | try 81 | { 82 | await Task.Delay(Interval, _cancellationTokenSource.Token).ConfigureAwait(false); 83 | await _action(_cancellationTokenSource.Token).ConfigureAwait(false); 84 | } 85 | catch (OperationCanceledException) when (_cancellationTokenSource.IsCancellationRequested) 86 | { 87 | break; 88 | } 89 | catch (TaskCanceledException) when (_cancellationTokenSource.IsCancellationRequested) 90 | { 91 | break; 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Swan.Logging/Logging/TextLogger.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Logging; 2 | 3 | using Extensions; 4 | using Formatters; 5 | using Platform; 6 | 7 | /// 8 | /// Use this class for text-based loggers. 9 | /// 10 | public abstract class TextLogger 11 | { 12 | /// 13 | /// Gets or sets the logging time format. 14 | /// set to null or empty to prevent output. 15 | /// 16 | /// 17 | /// The logging time format. 18 | /// 19 | public static string LoggingTimeFormat { get; set; } = "HH:mm:ss.fff"; 20 | 21 | /// 22 | /// Gets the color of the output of the message (the output message has a new line char in the end). 23 | /// 24 | /// The instance containing the event data. 25 | /// 26 | /// The output message formatted and the color of the console to be used. 27 | /// 28 | protected static (string outputMessage, ConsoleColor color) GetOutputAndColor(LogMessageReceivedEventArgs logEvent) 29 | { 30 | var (prefix, color) = GetConsoleColorAndPrefix(logEvent.MessageType); 31 | 32 | var loggerMessage = string.IsNullOrWhiteSpace(logEvent.Message) 33 | ? string.Empty 34 | : logEvent.Message.RemoveControlChars('\n'); 35 | 36 | var outputMessage = CreateOutputMessage(logEvent.Source, loggerMessage, prefix, logEvent.UtcDate); 37 | 38 | // Further format the output in the case there is an exception being logged 39 | if (logEvent.MessageType == LogLevel.Error && logEvent.Exception != null) 40 | { 41 | try 42 | { 43 | outputMessage += $"{logEvent.Exception.Stringify().Indent()}{Environment.NewLine}"; 44 | } 45 | catch 46 | { 47 | // Ignore 48 | } 49 | } 50 | 51 | return (outputMessage, color); 52 | } 53 | 54 | private static (string Prefix, ConsoleColor color) GetConsoleColorAndPrefix(LogLevel messageType) => 55 | messageType switch 56 | { 57 | LogLevel.Debug => (ConsoleLogger.DebugPrefix, ConsoleLogger.DebugColor), 58 | LogLevel.Error => (ConsoleLogger.ErrorPrefix, ConsoleLogger.ErrorColor), 59 | LogLevel.Info => (ConsoleLogger.InfoPrefix, ConsoleLogger.InfoColor), 60 | LogLevel.Trace => (ConsoleLogger.TracePrefix, ConsoleLogger.TraceColor), 61 | LogLevel.Warning => (ConsoleLogger.WarnPrefix, ConsoleLogger.WarnColor), 62 | LogLevel.Fatal => (ConsoleLogger.FatalPrefix, ConsoleLogger.FatalColor), 63 | _ => (new(' ', ConsoleLogger.InfoPrefix.Length), Terminal.Settings.DefaultColor) 64 | }; 65 | 66 | private static string CreateOutputMessage(string sourceName, string loggerMessage, string prefix, DateTime date) 67 | { 68 | var friendlySourceName = string.IsNullOrWhiteSpace(sourceName) 69 | ? string.Empty 70 | : sourceName.SliceLength(sourceName.LastIndexOf('.') + 1, sourceName.Length); 71 | 72 | var outputMessage = string.IsNullOrWhiteSpace(sourceName) 73 | ? loggerMessage 74 | : $"[{friendlySourceName}] {loggerMessage}"; 75 | 76 | return string.IsNullOrWhiteSpace(LoggingTimeFormat) 77 | ? $" {prefix} >> {outputMessage}{Environment.NewLine}" 78 | : $" {date.ToLocalTime().ToString(LoggingTimeFormat)} {prefix} >> {outputMessage}{Environment.NewLine}"; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Swan.Core/Gizmos/ValueCache.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Gizmos; 2 | 3 | /// 4 | /// Provides a thread-safe, lazy provider of cached values. 5 | /// 6 | /// The type of the keys. 7 | /// The type of the values. 8 | public class ValueCache 9 | where TKey : notnull 10 | { 11 | private readonly ConcurrentDictionary _dictionary = new(); 12 | private readonly SemaphoreSlim _semaphore = new(1, 1); 13 | 14 | /// 15 | /// Creates a new instance of the class. 16 | /// 17 | public ValueCache() 18 | { 19 | // placeholder 20 | } 21 | 22 | /// 23 | /// Gets a value indicating whether the value for the given key exists. 24 | /// 25 | /// The key. 26 | /// True if the key exists. False otherwise. 27 | public bool ContainsKey(TKey key) => _dictionary.ContainsKey(key); 28 | 29 | /// 30 | /// Gets a value if it exists in the cache or returns the default value. 31 | /// 32 | /// The key to test for. 33 | /// The value or its default. 34 | public TValue? GetValueOrDefault(TKey key) => _dictionary.TryGetValue(key, out var value) 35 | ? value 36 | : default; 37 | 38 | /// 39 | /// Gets a cached value. If the key is not found, the factory method is 40 | /// called, the result cached, and the value is returned. 41 | /// 42 | /// The key store. 43 | /// The factory method. 44 | /// The cached or newly created value. 45 | public TValue GetValue(TKey key, Func factory) 46 | { 47 | if (factory is null) 48 | throw new ArgumentNullException(nameof(factory)); 49 | 50 | if (_dictionary.TryGetValue(key, out var value)) 51 | return value; 52 | 53 | _dictionary[key] = value = factory(); 54 | return value; 55 | } 56 | 57 | /// 58 | /// Gets a cached value. If the key is not found, the factory method is 59 | /// called, the result cached, and the value is returned. 60 | /// 61 | /// The key store. 62 | /// The factory method. 63 | /// The cached or newly created value. 64 | public async Task GetValueAsync(TKey key, Func> factory) 65 | { 66 | if (factory is null) 67 | throw new ArgumentNullException(nameof(factory)); 68 | 69 | if (_dictionary.TryGetValue(key, out var value)) 70 | return value; 71 | 72 | try 73 | { 74 | await _semaphore.WaitAsync().ConfigureAwait(false); 75 | _dictionary[key] = value = await factory().ConfigureAwait(false); 76 | return value; 77 | } 78 | finally 79 | { 80 | _semaphore.Release(); 81 | } 82 | } 83 | 84 | /// 85 | /// Clears all the value caches. 86 | /// 87 | public void Clear() => _dictionary.Clear(); 88 | 89 | /// 90 | /// Clears the value stored in the specified key. 91 | /// 92 | /// The key of the value to clear. 93 | public void Clear(TKey key) => _dictionary.TryRemove(key, out _); 94 | } 95 | -------------------------------------------------------------------------------- /src/Swan.Data/Data/Schema/ITableBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace Swan.Data.Schema; 2 | 3 | /// 4 | /// Provides a table builder with a backing schema typically used to issue Table DDL commands. 5 | /// 6 | public interface ITableBuilder : IDbTableSchema, IDbConnected 7 | { 8 | /// 9 | /// Using the table schema, builds a DDL command to create the table in the database 10 | /// if it does not exist. 11 | /// 12 | /// The optional transaction. 13 | /// The command. 14 | DbCommand BuildTableCommand(DbTransaction? transaction = default); 15 | 16 | /// 17 | /// Executes the DDL command that creates the table if it does not exist. 18 | /// 19 | /// The optional transaction. 20 | /// The resulting table context from executing the DDL command. 21 | ITableContext ExecuteTableCommand(DbTransaction? transaction = default); 22 | 23 | /// 24 | /// Executes the DDL command that creates the table if it does not exist. 25 | /// 26 | /// The optional transaction. 27 | /// The cancellation token. 28 | /// The resulting table context from executing the DDL command. 29 | Task ExecuteTableCommandAsync(DbTransaction? transaction = default, CancellationToken ct = default); 30 | 31 | /// 32 | /// Adds a column to the table schema. 33 | /// Column name is mandatory. 34 | /// 35 | /// The column to add. 36 | ITableBuilder AddColumn(IDbColumnSchema column); 37 | 38 | /// 39 | /// Removes a column from the table schema by its column name. 40 | /// 41 | /// The name of the column to remove. 42 | ITableBuilder RemoveColumn(string columnName); 43 | } 44 | 45 | /// 46 | /// Represents a generic table builder. 47 | /// 48 | /// The entity type. 49 | public interface ITableBuilder : ITableBuilder 50 | where T : class 51 | { 52 | /// 53 | /// Executes the DDL command that creates the table if it does not exist. 54 | /// 55 | /// The optional transaction. 56 | /// The resulting table context from executing the DDL command. 57 | new ITableContext ExecuteTableCommand(DbTransaction? transaction = default); 58 | 59 | /// 60 | /// Executes the DDL command that creates the table if it does not exist. 61 | /// 62 | /// The optional transaction. 63 | /// The cancellation token. 64 | /// The resulting table context from executing the DDL command. 65 | new Task> ExecuteTableCommandAsync(DbTransaction? transaction = default, CancellationToken ct = default); 66 | 67 | /// 68 | /// Adds a column to the table schema. 69 | /// Column name is mandatory. 70 | /// 71 | /// The column to add. 72 | new ITableBuilder AddColumn(IDbColumnSchema column); 73 | 74 | /// 75 | /// Removes a column from the table schema by its column name. 76 | /// 77 | /// The name of the column to remove. 78 | new ITableBuilder RemoveColumn(string columnName); 79 | } 80 | -------------------------------------------------------------------------------- /src/Swan.Net/Smtp/Enums.Smtp.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable InconsistentNaming 2 | namespace Swan.Net.Smtp; 3 | 4 | /// 5 | /// Enumerates all of the well-known SMTP command names. 6 | /// 7 | public enum SmtpCommandNames 8 | { 9 | /// 10 | /// An unknown command 11 | /// 12 | Unknown, 13 | 14 | /// 15 | /// The helo command 16 | /// 17 | HELO, 18 | 19 | /// 20 | /// The ehlo command 21 | /// 22 | EHLO, 23 | 24 | /// 25 | /// The quit command 26 | /// 27 | QUIT, 28 | 29 | /// 30 | /// The help command 31 | /// 32 | HELP, 33 | 34 | /// 35 | /// The noop command 36 | /// 37 | NOOP, 38 | 39 | /// 40 | /// The rset command 41 | /// 42 | RSET, 43 | 44 | /// 45 | /// The mail command 46 | /// 47 | MAIL, 48 | 49 | /// 50 | /// The data command 51 | /// 52 | DATA, 53 | 54 | /// 55 | /// The send command 56 | /// 57 | SEND, 58 | 59 | /// 60 | /// The soml command 61 | /// 62 | SOML, 63 | 64 | /// 65 | /// The saml command 66 | /// 67 | SAML, 68 | 69 | /// 70 | /// The RCPT command 71 | /// 72 | RCPT, 73 | 74 | /// 75 | /// The vrfy command 76 | /// 77 | VRFY, 78 | 79 | /// 80 | /// The expn command 81 | /// 82 | EXPN, 83 | 84 | /// 85 | /// The starttls command 86 | /// 87 | STARTTLS, 88 | 89 | /// 90 | /// The authentication command 91 | /// 92 | AUTH, 93 | } 94 | 95 | /// 96 | /// Enumerates the reply code severities. 97 | /// 98 | public enum SmtpReplyCodeSeverities 99 | { 100 | /// 101 | /// The unknown severity 102 | /// 103 | Unknown = 0, 104 | 105 | /// 106 | /// The positive completion severity 107 | /// 108 | PositiveCompletion = 200, 109 | 110 | /// 111 | /// The positive intermediate severity 112 | /// 113 | PositiveIntermediate = 300, 114 | 115 | /// 116 | /// The transient negative severity 117 | /// 118 | TransientNegative = 400, 119 | 120 | /// 121 | /// The permanent negative severity 122 | /// 123 | PermanentNegative = 500, 124 | } 125 | 126 | /// 127 | /// Enumerates the reply code categories. 128 | /// 129 | public enum SmtpReplyCodeCategories 130 | { 131 | /// 132 | /// The unknown category 133 | /// 134 | Unknown = -1, 135 | 136 | /// 137 | /// The syntax category 138 | /// 139 | Syntax = 0, 140 | 141 | /// 142 | /// The information category 143 | /// 144 | Information = 1, 145 | 146 | /// 147 | /// The connections category 148 | /// 149 | Connections = 2, 150 | 151 | /// 152 | /// The unspecified a category 153 | /// 154 | UnspecifiedA = 3, 155 | 156 | /// 157 | /// The unspecified b category 158 | /// 159 | UnspecifiedB = 4, 160 | 161 | /// 162 | /// The system category 163 | /// 164 | System = 5, 165 | } 166 | --------------------------------------------------------------------------------