diff --git a/.gitignore b/.gitignore
index 04096015..c2977fea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -88,4 +88,5 @@ Desktop.ini
docs/Gemfile.lock
docs/.sass-cache/
docs/_site/
-_site/
\ No newline at end of file
+_site/
+appsettings.local.json
\ No newline at end of file
diff --git a/DBScripts/Oracle.sql b/DBScripts/Oracle.sql
new file mode 100644
index 00000000..1aad75c3
--- /dev/null
+++ b/DBScripts/Oracle.sql
@@ -0,0 +1,109 @@
+declare
+name_exists_exception exception;
+index_exists_exception exception;
+column_exists_exception exception;
+column_not_exists_exception exception;
+pragma exception_init( name_exists_exception, -955 );
+pragma exception_init( index_exists_exception, -1408 );
+pragma exception_init( column_exists_exception, -1430 );
+pragma exception_init( column_not_exists_exception, -904 );
+begin
+ begin
+ execute immediate 'CREATE TABLE Exceptions(
+ Id number generated always as Identity(start with 1 increment by 1),
+ GUID varchar2(36) NOT NULL,
+ ApplicationName varchar2(50) NOT NULL,
+ MachineName varchar2(50) NOT NULL,
+ CreationDate timestamp NOT NULL,
+ "TYPE" varchar2(100) NOT NULL,
+ IsProtected number(1) default 0 NOT NULL,
+ Host varchar2(100) NULL,
+ Url varchar2(500) NULL,
+ HTTPMethod varchar2(10) NULL,
+ IPAddress varchar2(40) NULL,
+ "SOURCE" varchar2(100) NULL,
+ Message varchar2(1000) NULL,
+ Detail Nclob NULL,
+ StatusCode number NULL,
+ DeletionDate timestamp NULL,
+ FullJson Nclob NULL,
+ ErrorHash number NULL,
+ DuplicateCount number default 1 NOT NULL,
+ LastLogDate timestamp NULL,
+ Category varchar2(100) NULL
+ )';
+ exception
+ when name_exists_exception then
+ null;
+ end;
+
+ begin
+ execute immediate('CREATE INDEX IX_Exceptions_One ON Exceptions(GUID, ApplicationName, DeletionDate, CreationDate DESC)');
+ exception
+ when index_exists_exception then
+ null;
+ when name_exists_exception then
+ null;
+ end;
+
+ begin
+ execute immediate('CREATE INDEX IX_Exceptions_Two ON Exceptions(ErrorHash, ApplicationName, CreationDate DESC, DeletionDate)');
+ exception
+ when index_exists_exception then
+ null;
+ when name_exists_exception then
+ null;
+ end;
+
+ --Begin V2 Schema changes
+
+ begin
+ execute immediate('ALTER TABLE Exceptions ADD LastLogDate timestamp NULL');
+ exception
+ when column_exists_exception then
+ null;
+ end;
+
+ begin
+ execute immediate('ALTER TABLE Exceptions ADD Category varchar2(100) NULL');
+ exception
+ when column_exists_exception then
+ null;
+ end;
+
+ begin
+ execute immediate('ALTER TABLE Exceptions DROP COLUMN SQL');
+ exception
+ when column_not_exists_exception then
+ null;
+ end;
+end;
+/
+
+create or replace
+ function ExceptionsLogUpdate(p_DuplicateCount in number,
+ p_CreationDate in timestamp,
+ p_ErrorHash in number,
+ p_ApplicationName in varchar2,
+ p_MinDate in timestamp)
+ return varchar2 is
+ pragma autonomous_transaction;
+ begin
+ declare
+ v_guid varchar2(36);
+ begin
+
+ update Exceptions
+ set DuplicateCount = DuplicateCount + p_DuplicateCount,
+ LastLogDate = (Case When LastLogDate Is Null Or p_CreationDate > LastLogDate Then p_CreationDate Else LastLogDate End)
+ where ErrorHash = p_ErrorHash
+ and ApplicationName = p_ApplicationName
+ and DeletionDate Is Null
+ and CreationDate >= p_MinDate
+ and rownum = 1
+ returning GUID into v_guid;
+ commit;
+
+ return v_guid;
+ end;
+ end;
\ No newline at end of file
diff --git a/StackExchange.Exceptional.sln b/StackExchange.Exceptional.sln
index 06e8cefe..676c68bf 100644
--- a/StackExchange.Exceptional.sln
+++ b/StackExchange.Exceptional.sln
@@ -35,6 +35,7 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DBScripts", "DBScripts", "{7669A017-BBDA-45B0-89B1-A9E0F2FD0BF8}"
ProjectSection(SolutionItems) = preProject
DBScripts\MySQL.sql = DBScripts\MySQL.sql
+ DBScripts\Oracle.sql = DBScripts\Oracle.sql
DBScripts\PostgreSql.sql = DBScripts\PostgreSql.sql
DBScripts\SqlServer.sql = DBScripts\SqlServer.sql
EndProjectSection
@@ -56,7 +57,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StackExchange.Exceptional.P
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StackExchange.Exceptional.MongoDB", "src\StackExchange.Exceptional.MongoDB\StackExchange.Exceptional.MongoDB.csproj", "{8CFA59A5-5180-4466-A32E-507F3D541163}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples.ConsoleNetCore", "samples\Samples.ConsoleNetCore\Samples.ConsoleNetCore.csproj", "{6CE269E1-6DC9-43A4-B6B8-683CE8A19E6A}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples.ConsoleNetCore", "samples\Samples.ConsoleNetCore\Samples.ConsoleNetCore.csproj", "{6CE269E1-6DC9-43A4-B6B8-683CE8A19E6A}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StackExchange.Exceptional.Oracle", "src\StackExchange.Exceptional.Oracle\StackExchange.Exceptional.Oracle.csproj", "{A51A932D-67DB-4A1C-89D2-886DE2258E58}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -112,6 +115,10 @@ Global
{6CE269E1-6DC9-43A4-B6B8-683CE8A19E6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6CE269E1-6DC9-43A4-B6B8-683CE8A19E6A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6CE269E1-6DC9-43A4-B6B8-683CE8A19E6A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A51A932D-67DB-4A1C-89D2-886DE2258E58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A51A932D-67DB-4A1C-89D2-886DE2258E58}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A51A932D-67DB-4A1C-89D2-886DE2258E58}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A51A932D-67DB-4A1C-89D2-886DE2258E58}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -129,6 +136,7 @@ Global
{0A412433-C2CE-4EBE-BB5A-C3E48983E941} = {8B91532F-A112-4F73-80BD-A95B7359BBC3}
{8CFA59A5-5180-4466-A32E-507F3D541163} = {8B91532F-A112-4F73-80BD-A95B7359BBC3}
{6CE269E1-6DC9-43A4-B6B8-683CE8A19E6A} = {001E5AA4-42C8-4AC3-B14A-AF1DFA02E9FB}
+ {A51A932D-67DB-4A1C-89D2-886DE2258E58} = {8B91532F-A112-4F73-80BD-A95B7359BBC3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7F066A86-14D8-4A2C-848A-D4371BB704FA}
diff --git a/src/StackExchange.Exceptional.Oracle/OracleClobParameter.cs b/src/StackExchange.Exceptional.Oracle/OracleClobParameter.cs
new file mode 100644
index 00000000..2b47c7c7
--- /dev/null
+++ b/src/StackExchange.Exceptional.Oracle/OracleClobParameter.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Dapper;
+using Oracle.ManagedDataAccess.Client;
+using Oracle.ManagedDataAccess.Types;
+
+namespace StackExchange.Exceptional.Stores
+{
+ ///
+ /// Needed because long strings can not be passed.
+ ///
+ ///
+ /// https://github.com/StackExchange/Dapper/issues/142
+ ///
+ internal class OracleClobParameter : SqlMapper.ICustomQueryParameter
+ {
+ private readonly string value;
+
+ public OracleClobParameter(string value)
+ {
+ this.value = value;
+ }
+
+ public void AddParameter(IDbCommand command, string name)
+ {
+ // accesing the connection in open state.
+ var connection = command.Connection as OracleConnection;
+
+ connection.StateChange += (object sender, StateChangeEventArgs e) =>
+ {
+ if (e.CurrentState != ConnectionState.Open)
+ return;
+
+ var clob = new OracleClob(connection);
+
+ // It should be Unicode oracle throws an exception when
+ // the length is not even.
+ var bytes = System.Text.Encoding.Unicode.GetBytes(value);
+ var length = System.Text.Encoding.Unicode.GetByteCount(value);
+
+ int pos = 0;
+ int chunkSize = 1024; // Oracle does not allow large chunks.
+
+ while (pos < length)
+ {
+ chunkSize = chunkSize > (length - pos) ? chunkSize = length - pos : chunkSize;
+ clob.Write(bytes, pos, chunkSize);
+ pos += chunkSize;
+ }
+
+ var param = new OracleParameter(name, OracleDbType.Clob);
+ param.Value = clob;
+
+ command.Parameters.Add(param);
+ };
+ }
+ }
+}
diff --git a/src/StackExchange.Exceptional.Oracle/OracleErrorStore.cs b/src/StackExchange.Exceptional.Oracle/OracleErrorStore.cs
new file mode 100644
index 00000000..aacb6c05
--- /dev/null
+++ b/src/StackExchange.Exceptional.Oracle/OracleErrorStore.cs
@@ -0,0 +1,379 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Dapper;
+using StackExchange.Exceptional.Internal;
+using Oracle.ManagedDataAccess.Client;
+
+namespace StackExchange.Exceptional.Stores
+{
+ ///
+ /// An implementation that uses Oracle as its backing store.
+ ///
+ public sealed class OracleErrorStore : ErrorStore
+ {
+ ///
+ /// Name for this error store.
+ ///
+ public override string Name => "Oracle Error Store";
+
+ private readonly string _tableName;
+ private readonly int _displayCount;
+ private readonly string _connectionString;
+
+ ///
+ /// The maximum count of errors to show.
+ ///
+ public const int MaximumDisplayCount = 500;
+
+ ///
+ /// Creates a new instance of with the specified connection string.
+ /// The default table name is "Exceptions".
+ ///
+ /// The database connection string to use.
+ /// The application name to use when logging.
+ public OracleErrorStore(string connectionString, string applicationName)
+ : this(new ErrorStoreSettings()
+ {
+ ApplicationName = applicationName,
+ ConnectionString = connectionString
+ })
+ { }
+
+ ///
+ /// Creates a new instance of with the given configuration.
+ /// The default table name is "Exceptions".
+ ///
+ /// The for this store.
+ public OracleErrorStore(ErrorStoreSettings settings) : base(settings)
+ {
+ _displayCount = Math.Min(settings.Size, MaximumDisplayCount);
+ _connectionString = settings.ConnectionString;
+ _tableName = settings.TableName ?? "Exceptions";
+
+ if (_connectionString.IsNullOrEmpty())
+ throw new ArgumentOutOfRangeException(nameof(settings), "A connection string or connection string name must be specified when using a Oracle error store");
+
+ this.ChangeGuidHandler();
+ }
+
+ private string _sqlProtectError;
+ private string SqlProtectError => _sqlProtectError ?? (_sqlProtectError = $@"
+Update {_tableName}
+ Set IsProtected = 1, DeletionDate = Null
+ Where lower(GUID) = :guid");
+
+ ///
+ /// Protects an error from deletion, by making IsProtected = 1 in the database.
+ ///
+ /// The GUID of the error to protect.
+ /// true if the error was found and protected, false otherwise.
+ protected override async Task ProtectErrorAsync(Guid guid)
+ {
+ using (var c = GetConnection())
+ {
+ return await c.ExecuteAsync(SqlProtectError, new { guid = this.GuidToSting(guid) }).ConfigureAwait(false) > 0;
+ }
+ }
+
+ private string _sqlProtectErrors;
+ private string SqlProtectErrors => _sqlProtectErrors ?? (_sqlProtectErrors = $@"
+Update {_tableName}
+ Set IsProtected = 1, DeletionDate = Null
+ Where lower(GUID) In :guids");
+
+ ///
+ /// Protects errors from deletion, by making IsProtected = 1 in the database.
+ ///
+ /// The GUIDs of the errors to protect.
+ /// true if the errors were found and protected, false otherwise.
+ protected override async Task ProtectErrorsAsync(IEnumerable guids)
+ {
+ using (var c = GetConnection())
+ {
+ return await c.ExecuteAsync(SqlProtectErrors, new { guids = this.GuidsToStings(guids) }).ConfigureAwait(false) > 0;
+ }
+ }
+
+ private string _sqlDeleteError;
+ private string SqlDeleteError => _sqlDeleteError ?? (_sqlDeleteError = $@"
+Update {_tableName}
+ Set DeletionDate = CURRENT_TIMESTAMP
+ Where lower(GUID) = :guid
+ And DeletionDate Is Null");
+
+ ///
+ /// Deletes an error, by setting DeletionDate = UTC_DATE() in SQL.
+ ///
+ /// The GUID of the error to delete.
+ /// true if the error was found and deleted, false otherwise.
+ protected override async Task DeleteErrorAsync(Guid guid)
+ {
+ using (var c = GetConnection())
+ {
+ return await c.ExecuteAsync(SqlDeleteError, new { guid = this.GuidToSting(guid), ApplicationName }).ConfigureAwait(false) > 0;
+ }
+ }
+
+ private string _sqlDeleteErrors;
+ private string SqlDeleteErrors => _sqlDeleteErrors ?? (_sqlDeleteErrors = $@"
+Update {_tableName}
+ Set DeletionDate = CURRENT_TIMESTAMP
+ Where lower(GUID) In :guids
+ And DeletionDate Is Null");
+
+ ///
+ /// Deletes errors, by setting DeletionDate = UTC_DATE() in SQL.
+ ///
+ /// The GUIDs of the errors to delete.
+ /// true if the errors were found and deleted, false otherwise.
+ protected override async Task DeleteErrorsAsync(IEnumerable guids)
+ {
+ using (var c = GetConnection())
+ {
+ return await c.ExecuteAsync(SqlDeleteErrors, new { guids = this.GuidsToStings(guids) }).ConfigureAwait(false) > 0;
+ }
+ }
+
+ private string _sqlHardDeleteErrors;
+ private string SqlHardDeleteErrors => _sqlHardDeleteErrors ?? (_sqlHardDeleteErrors = $@"
+Delete From {_tableName}
+ Where lower(GUID) = :guid
+ And ApplicationName = :ApplicationName");
+
+ ///
+ /// Hard deletes an error, actually deletes the row from SQL rather than setting .
+ /// This is used to cleanup when testing the error store when attempting to come out of retry/failover mode after losing connection to SQL.
+ ///
+ /// The GUID of the error to hard delete.
+ /// true if the error was found and deleted, false otherwise.
+ protected override async Task HardDeleteErrorAsync(Guid guid)
+ {
+ using (var c = GetConnection())
+ {
+ return await c.ExecuteAsync(SqlHardDeleteErrors, new { guid = this.GuidToSting(guid), ApplicationName }).ConfigureAwait(false) > 0;
+ }
+ }
+
+ private string _sqlDeleteAllErrors;
+ private string SqlDeleteAllErrors => _sqlDeleteAllErrors ?? (_sqlDeleteAllErrors = $@"
+Update {_tableName}
+ Set DeletionDate = CURRENT_TIMESTAMP
+ Where DeletionDate Is Null
+ And IsProtected = 0
+ And ApplicationName = :ApplicationName");
+
+ ///
+ /// Deleted all errors in the log, by setting = UTC_DATE() in SQL.
+ ///
+ /// The name of the application to delete all errors for.
+ /// true if any errors were deleted, false otherwise.
+ protected override async Task DeleteAllErrorsAsync(string applicationName = null)
+ {
+ using (var c = GetConnection())
+ {
+ return await c.ExecuteAsync(SqlDeleteAllErrors, new { ApplicationName = applicationName ?? ApplicationName }).ConfigureAwait(false) > 0;
+ }
+ }
+
+ private string _sqlLogUpdate;
+ private string SqlLogUpdate => _sqlLogUpdate ?? (_sqlLogUpdate = $@"Select {_tableName}LogUpdate(:DuplicateCount, :CreationDate, :ErrorHash, :ApplicationName, :minDate) from Dual");
+
+ private string _sqlLogInsert;
+ private string SqlLogInsert => _sqlLogInsert ?? (_sqlLogInsert = $@"
+Insert Into {_tableName} (GUID, ApplicationName, Category, MachineName, CreationDate, Type, IsProtected, Host, Url, HTTPMethod, IPAddress, Source, Message, Detail, StatusCode, FullJson, ErrorHash, DuplicateCount, LastLogDate)
+Values (:GUID, :ApplicationName, :Category, :MachineName, :CreationDate, :Type, :IsProtected, :Host, :Url, :HTTPMethod, :IPAddress, :Source, :Message, :Detail, :StatusCode, :FullJson, :ErrorHash, :DuplicateCount, :LastLogDate)");
+
+ private DynamicParameters GetUpdateParams(Error error) =>
+ new DynamicParameters(new
+ {
+ error.DuplicateCount,
+ error.ErrorHash,
+ error.CreationDate,
+ ApplicationName = error.ApplicationName.Truncate(50),
+ minDate = DateTime.UtcNow.Subtract(Settings.RollupPeriod.Value)
+ });
+
+ private object GetInsertParams(Error error) => new
+ {
+ Guid = this.GuidToSting(error.GUID),
+ ApplicationName = error.ApplicationName.Truncate(50),
+ Category = error.Category.Truncate(100),
+ MachineName = error.MachineName.Truncate(50),
+ error.CreationDate,
+ Type = error.Type.Truncate(100),
+ IsProtected = error.IsProtected ? 1 : 0,
+ Host = error.Host.Truncate(100),
+ Url = error.UrlPath.Truncate(500),
+ HTTPMethod = error.HTTPMethod.Truncate(10),
+ error.IPAddress,
+ Source = error.Source.Truncate(100),
+ Message = error.Message.Truncate(1000),
+ Detail = new OracleClobParameter(error.Detail) ,
+ error.StatusCode,
+ FullJson = new OracleClobParameter(error.FullJson),
+ error.ErrorHash,
+ error.DuplicateCount,
+ error.LastLogDate
+ };
+
+ ///
+ /// Logs the error to SQL.
+ /// If the roll-up conditions are met, then the matching error will have a
+ /// DuplicateCount += @DuplicateCount (usually 1, unless in retry) rather than a distinct new row for the error.
+ ///
+ /// The error to log.
+ protected override bool LogError(Error error)
+ {
+ using (var c = GetConnection())
+ {
+ if (Settings.RollupPeriod.HasValue && error.ErrorHash.HasValue)
+ {
+ var queryParams = GetUpdateParams(error);
+ var guid = c.QueryFirstOrDefault(SqlLogUpdate, queryParams);
+ // if we found an exception that's a duplicate, jump out
+ if (guid != null)
+ {
+ error.GUID = Guid.Parse(guid);
+ return true;
+ }
+ }
+
+ error.FullJson = error.ToJson();
+ return c.Execute(SqlLogInsert, GetInsertParams(error)) > 0;
+ }
+ }
+
+ ///
+ /// Asynchronously logs the error to SQL.
+ /// If the roll-up conditions are met, then the matching error will have a
+ /// DuplicateCount += @DuplicateCount (usually 1, unless in retry) rather than a distinct new row for the error.
+ ///
+ /// The error to log.
+ protected override async Task LogErrorAsync(Error error)
+ {
+ using (var c = GetConnection())
+ {
+ if (Settings.RollupPeriod.HasValue && error.ErrorHash.HasValue)
+ {
+ var queryParams = GetUpdateParams(error);
+ var guid = await c.QueryFirstOrDefaultAsync(SqlLogUpdate, queryParams).ConfigureAwait(false);
+ // if we found an exception that's a duplicate, jump out
+ if (guid != null)
+ {
+ error.GUID = Guid.Parse(guid);
+ return true;
+ }
+ }
+
+ error.FullJson = error.ToJson();
+
+ return (await c.ExecuteAsync(SqlLogInsert, GetInsertParams(error)).ConfigureAwait(false)) > 0;
+ }
+ }
+
+ private string _sqlGetError;
+ private string SqlGetError => _sqlGetError ?? (_sqlGetError = $@"
+Select *
+ From {_tableName}
+ Where lower(GUID) = :guid");
+
+ ///
+ /// Gets the error with the specified GUID from SQL.
+ /// This can return a deleted error as well, there's no filter based on .
+ ///
+ /// The GUID of the error to retrieve.
+ /// The error object if found, null otherwise.
+ protected override async Task GetErrorAsync(Guid guid)
+ {
+ Error sqlError;
+ using (var c = GetConnection())
+ {
+ sqlError = await c.QueryFirstOrDefaultAsync(SqlGetError, new { guid = this.GuidToSting(guid) }).ConfigureAwait(false);
+ }
+ if (sqlError == null) return null;
+
+ // everything is in the JSON, but not the columns and we have to deserialize for collections anyway
+ // so use that deserialized version and just get the properties that might change on the SQL side and apply them
+ var result = Error.FromJson(sqlError.FullJson);
+ result.DuplicateCount = sqlError.DuplicateCount;
+ result.DeletionDate = sqlError.DeletionDate;
+ result.IsProtected = sqlError.IsProtected;
+ result.LastLogDate = sqlError.LastLogDate;
+ return result;
+ }
+
+ private string _sqlGetAllErrors;
+ private string SqlGetAllErrors => _sqlGetAllErrors ?? (_sqlGetAllErrors = $@"
+Select *
+ From {_tableName}
+ Where DeletionDate Is Null
+ And ApplicationName = :ApplicationName
+Order By CreationDate Desc fetch first :max rows only");
+
+ ///
+ /// Retrieves all non-deleted application errors in the database.
+ ///
+ /// The name of the application to get all errors for.
+ protected override async Task> GetAllErrorsAsync(string applicationName = null)
+ {
+ using (var c = GetConnection())
+ {
+ return (await c.QueryAsync(SqlGetAllErrors, new { max = _displayCount, ApplicationName = applicationName ?? ApplicationName }).ConfigureAwait(false)).AsList();
+ }
+ }
+
+ private string _sqlGetErrorCount;
+ private string SqlGetErrorCount => _sqlGetErrorCount ?? (_sqlGetErrorCount = $@"
+Select Count(*)
+ From {_tableName}
+ Where DeletionDate Is Null
+ And ApplicationName = :ApplicationName");
+
+ private string _sqlGetErrorCountWithSince;
+ private string SqlGetErrorCountWithSince => _sqlGetErrorCountWithSince ?? (_sqlGetErrorCountWithSince = $@"
+Select Count(*)
+ From {_tableName}
+ Where DeletionDate Is Null
+ And ApplicationName = :ApplicationName
+ And CreationDate > :since");
+
+ ///
+ /// Retrieves a count of application errors since the specified date, or all time if null.
+ ///
+ /// The date to get errors since.
+ /// The application name to get an error count for.
+ protected override async Task GetErrorCountAsync(DateTime? since = null, string applicationName = null)
+ {
+ using (var c = GetConnection())
+ {
+ return await c.QueryFirstOrDefaultAsync(
+ since.HasValue ? SqlGetErrorCountWithSince : SqlGetErrorCount,
+ new { since, ApplicationName = applicationName ?? ApplicationName }
+ ).ConfigureAwait(false);
+ }
+ }
+
+ private OracleConnection GetConnection() => new OracleConnection(_connectionString);
+
+ private void ChangeGuidHandler()
+ {
+ SqlMapper.AddTypeHandler(new OracleGuidTypeHandler());
+ }
+
+ private string GuidToSting(Guid target)
+ {
+ return target.ToString("N").ToLower();
+ }
+
+ private IEnumerable GuidsToStings(IEnumerable targets)
+ {
+ var result = new List();
+
+ foreach (var item in targets)
+ result.Add(GuidToSting(item));
+
+ return result;
+ }
+ }
+}
diff --git a/src/StackExchange.Exceptional.Oracle/OracleGuidTypeHandler.cs b/src/StackExchange.Exceptional.Oracle/OracleGuidTypeHandler.cs
new file mode 100644
index 00000000..407339b2
--- /dev/null
+++ b/src/StackExchange.Exceptional.Oracle/OracleGuidTypeHandler.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Dapper;
+
+namespace StackExchange.Exceptional.Stores
+{
+ ///
+ /// We need a special Mapping for Oracle.
+ ///
+ public class OracleGuidTypeHandler : SqlMapper.TypeHandler
+ {
+ public override void SetValue(IDbDataParameter parameter, Guid guid)
+ {
+ parameter.Value = guid.ToString("N");
+ }
+
+ public override Guid Parse(object value)
+ {
+ return new Guid((string)value);
+ }
+ }
+}
diff --git a/src/StackExchange.Exceptional.Oracle/StackExchange.Exceptional.Oracle.csproj b/src/StackExchange.Exceptional.Oracle/StackExchange.Exceptional.Oracle.csproj
new file mode 100644
index 00000000..12a64092
--- /dev/null
+++ b/src/StackExchange.Exceptional.Oracle/StackExchange.Exceptional.Oracle.csproj
@@ -0,0 +1,13 @@
+
+
+ StackExchange.Exceptional.Oracle
+ Oracle storage provider for StackExchange.Exceptional
+ Oracle Exception Handler Errors Stack Exchange Exceptional
+ net461;netstandard2.0
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/StackExchange.Exceptional.Tests.AspNetCore/Configs/Storage.Oracle.json b/tests/StackExchange.Exceptional.Tests.AspNetCore/Configs/Storage.Oracle.json
new file mode 100644
index 00000000..35d8f799
--- /dev/null
+++ b/tests/StackExchange.Exceptional.Tests.AspNetCore/Configs/Storage.Oracle.json
@@ -0,0 +1,9 @@
+{
+ "Exceptional": {
+ "Store": {
+ "ApplicationName": "Samples (ASP.NET Core Oracle)",
+ "Type": "Oracle",
+ "ConnectionString": "User Id=root;Password=root;Data Source=127.0.0.1:1521"
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/StackExchange.Exceptional.Tests.AspNetCore/Configuration.cs b/tests/StackExchange.Exceptional.Tests.AspNetCore/Configuration.cs
index bc133d9b..5c0ea50c 100644
--- a/tests/StackExchange.Exceptional.Tests.AspNetCore/Configuration.cs
+++ b/tests/StackExchange.Exceptional.Tests.AspNetCore/Configuration.cs
@@ -207,6 +207,32 @@ public void MongoDBStorage()
Assert.Equal("Samples (ASP.NET Core MongoDB)", store.ApplicationName);
}
+ [Fact]
+ public void OracleStorage()
+ {
+ var config = new ConfigurationBuilder()
+ .SetBasePath(Directory.GetCurrentDirectory())
+ .AddJsonFile(GetPath("Storage.Oracle.json"))
+ .Build();
+
+ Assert.NotNull(config);
+ var exceptionalSection = config.GetSection("Exceptional");
+ Assert.NotNull(exceptionalSection);
+
+ var settings = new ExceptionalSettings();
+ exceptionalSection.Bind(settings);
+
+ // Store
+ Assert.NotNull(settings.Store);
+ Assert.Equal("Samples (ASP.NET Core Oracle)", settings.Store.ApplicationName);
+ Assert.Equal("Oracle", settings.Store.Type);
+ Assert.Equal("User Id=root;Password=root;Data Source=127.0.0.1:1521", settings.Store.ConnectionString);
+
+ Assert.IsType(settings.DefaultStore);
+ var sqlStore = settings.DefaultStore as OracleErrorStore;
+ Assert.Equal("Samples (ASP.NET Core Oracle)", sqlStore.ApplicationName);
+ }
+
private string GetPath(string configFile) => Path.Combine("Configs", configFile);
}
}
diff --git a/tests/StackExchange.Exceptional.Tests.AspNetCore/StackExchange.Exceptional.Tests.AspNetCore.csproj b/tests/StackExchange.Exceptional.Tests.AspNetCore/StackExchange.Exceptional.Tests.AspNetCore.csproj
index 522d79af..31217770 100644
--- a/tests/StackExchange.Exceptional.Tests.AspNetCore/StackExchange.Exceptional.Tests.AspNetCore.csproj
+++ b/tests/StackExchange.Exceptional.Tests.AspNetCore/StackExchange.Exceptional.Tests.AspNetCore.csproj
@@ -7,6 +7,9 @@
+
+
+
diff --git a/tests/StackExchange.Exceptional.Tests/Helpers/TestConfig.cs b/tests/StackExchange.Exceptional.Tests/Helpers/TestConfig.cs
index 3d8fd079..06a48287 100644
--- a/tests/StackExchange.Exceptional.Tests/Helpers/TestConfig.cs
+++ b/tests/StackExchange.Exceptional.Tests/Helpers/TestConfig.cs
@@ -36,6 +36,7 @@ public class Config
public string MySQLConnectionString { get; set; } = Environment.GetEnvironmentVariable(nameof(MySQLConnectionString)) ?? "server=localhost;uid=root;pwd=root;database=test;Allow User Variables=true";
public string PostgreSqlConnectionString { get; set; } = Environment.GetEnvironmentVariable(nameof(PostgreSqlConnectionString)) ?? "Server=localhost;Port=5432;Database=test;User Id=postgres;Password=postgres;";
public string MongoDBConnectionString { get; set; } = Environment.GetEnvironmentVariable(nameof(MongoDBConnectionString)) ?? "mongodb://localhost/test";
+ public string OracleConnectionString { get; set; } = Environment.GetEnvironmentVariable(nameof(OracleConnectionString)) ?? "User Id=root;Password=root;Data Source=127.0.0.0:1521";
}
}
}
diff --git a/tests/StackExchange.Exceptional.Tests/StackExchange.Exceptional.Tests.csproj b/tests/StackExchange.Exceptional.Tests/StackExchange.Exceptional.Tests.csproj
index 20e20732..b044e1b6 100644
--- a/tests/StackExchange.Exceptional.Tests/StackExchange.Exceptional.Tests.csproj
+++ b/tests/StackExchange.Exceptional.Tests/StackExchange.Exceptional.Tests.csproj
@@ -6,6 +6,7 @@
+
diff --git a/tests/StackExchange.Exceptional.Tests/Storage/OracleErrorStoreTest.cs b/tests/StackExchange.Exceptional.Tests/Storage/OracleErrorStoreTest.cs
new file mode 100644
index 00000000..f928638b
--- /dev/null
+++ b/tests/StackExchange.Exceptional.Tests/Storage/OracleErrorStoreTest.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Runtime.CompilerServices;
+using Dapper;
+using Oracle.ManagedDataAccess.Client;
+using StackExchange.Exceptional.Stores;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace StackExchange.Exceptional.Tests.Storage
+{
+ public class OracleErrorStoreTest : StoreBaseTest, IClassFixture
+ {
+ public string ConnectionString => TestConfig.Current.OracleConnectionString;
+ private OracleFixture Fixture { get; }
+
+ public OracleErrorStoreTest(OracleFixture fixture, ITestOutputHelper output) : base(output)
+ {
+ Fixture = fixture;
+ if (Fixture.ShouldSkip)
+ {
+ Skip.Inconclusive("Couldn't test against: " + ConnectionString + "\n" + fixture.SkipReason);
+ }
+ }
+
+ protected override ErrorStore GetStore([CallerMemberName]string appName = null) =>
+ new OracleErrorStore(new ErrorStoreSettings
+ {
+ ConnectionString = ConnectionString,
+ ApplicationName = appName,
+ TableName = Fixture.TableName
+ });
+ }
+
+ public class OracleFixture : IDisposable
+ {
+ public bool ShouldSkip { get; }
+ public string SkipReason { get; }
+ public string TableName { get; }
+ public string TableScript { get; }
+ private string ConnectionString { get; }
+
+ public OracleFixture()
+ {
+ Skip.IfNoConfig(nameof(TestConfig.Current.OracleConnectionString), TestConfig.Current.OracleConnectionString);
+ try
+ {
+ var script = Resource.Get("Scripts.Oracle.sql");
+ var csb = new OracleConnectionStringBuilder(TestConfig.Current.OracleConnectionString)
+ {
+ ConnectionTimeout = 2000
+ };
+ ConnectionString = csb.ConnectionString;
+ using (var conn = new OracleConnection(ConnectionString))
+ {
+ TableName = "Test" + Guid.NewGuid().ToString("N").Substring(24);
+ TableScript = script.Replace("Exceptions", TableName);
+
+ //we have to split the script
+ foreach (var scriptPart in TableScript.Split('/'))
+ {
+ conn.Execute(scriptPart);
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ e.MaybeLog(TestConfig.Current.OracleConnectionString);
+ ShouldSkip = true;
+ SkipReason = e.Message;
+ }
+ }
+
+ public void Dispose()
+ {
+ try
+ {
+ using (var conn = new OracleConnection(ConnectionString))
+ {
+ conn.Execute("Drop Table " + TableName.ToUpper());
+ }
+ }
+ catch when (ShouldSkip)
+ {
+ // if we didn't error initially then we'll throw
+ }
+ }
+ }
+}