At Hackney, we have created NuGet Packages to prevent the duplication of common code when implementing our APIs. Hence this NuGet package will store common code that can then be used in relevant projects.
The pipeline automatically updates the version number for all packages in Hackney.Core.
Any specific version number follows the form Major.Minor.Patch[-Suffix]
, where the components have the following meanings:
- Major: Breaking changes
- Minor: New features, but backward compatible
- Patch: Backwards compatible bug fixes only
- Suffix (optional): a hyphen followed by a string denoting a pre-release version
In order for the pipeline to be able to run automated tests and create preview versions of packages, you must name your branch correctly.
Name your branch following the convention of feature/<some-feature>
. This will allow the pipeline to work correctly.
If all tests pass, a new version of your package will be publised on every commit. You can see published versions of packages here.
All preview versions of packages will have the suffix -feat-<branch-name>-<number>
.
This branch name in the package version has a character limit of 12 characters, so try to name your branch accordingly, otherwise it will be cut off.
After cloning the repo, you may find many errors relating to the Hackney.Core.Testing.PactBroker
project similar to the one below when attempting to build the solution on a Windows machine:
C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\Microsoft.Common.CurrentVersion.targets(4965,5): warning MSB3026: Could not copy "C:\Users\<USER-NAME>\.nuget\packages\pactnet.windows\3.0.2\tools\pact-win32\lib\ruby\lib\ruby\gems\2.2.0\gems\bundler-1.9.9\lib\bundler\vendor\thor\lib\thor\actions\empty_directory.rb" to "bin\Debug\netcoreapp3.1\pact-win32\lib\ruby\lib\ruby\gems\2.2.0\gems\bundler-1.9.9\lib\bundler\vendor\thor\lib\thor\actions\empty_directory.rb". Beginning retry 1 in 1000ms. Could not find a part of the path 'bin\Debug\netcoreapp3.1\pact-win32\lib\ruby\lib\ruby\gems\2.2.0\gems\bundler-1.9.9\lib\bundler\vendor\thor\lib\thor\actions\empty_directory.rb'.
This is because packages required by the PactBroker client libraries contain deep folder trees and when copied to the build output folder may then have filenames longer than that traditionally supported by Windows.
This can be fixed by enabling long paths in the Windows registry using these instructions. Once enabled it will take effect without needing to restart Visual Studio.
For full details on how to use the package(s) within this repository please read this wiki page.
Note: The Hackney.Core project has been split into individual packages and is now deprecated. In order to use our packages, import each Hackney.Core dependency required individually.
Please refer to our documentation on creating NuGet packages. For this repository, create your project folder in the Hackney.Core
folder and test folder in Hackney.Core.Tests
. Use the workflow template to create your own workflow file in the .github/workflows
folder.
The following features are implemented within this package (Alphabetical Order):
- Authorisation
- DynamoDb
- ElasticSearch
- Enums
- Health check helpers
- Http
- JWT
- Logging
- MVC Middleware
- Sns
- Testing
- Validation
- Validation.AspNet
Project reference: Hackney.Core.Middleware
There are a number of different middleware classes implemented here.
Please bear in mind that the order in which they are used within an application's Startup.Configure()
method dictates the order in which they are executed when the application receives an HTTP request.
The correlation middleware can be used to ensure that all incoming HTTP requests contain a correlation id value.
If a request does not contain a guid HTTP header value with the key x-correlation-id
then one is generated and added to the request headers.
The correlation id is also automatically added to the response headers.
Correlation Id - This is a (guid) value used to uniquely identify each request.
using Hackney.Core.Middleware.Correlation;
namespace SomeApi
{
public class Startup
{
...
public static void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseCorrelationId();
...
}
}
}
The exception middleware can be used to set up a custom exception handler that will be used in the event of any unhandled exception. It will log the exception and then return a standard error response that looks like the following.
{
"message": "String '2020-09-0 17:33:44' was not recognized as a valid DateTime.",
"traceId": "0HM8BQ8L7EAEO:00000001",
"correlationId": "3fbf8755-eb41-4c03-be9f-d0ccae470e39",
"statusCode": 500
}
Property | Description |
---|---|
message | The message property from the unhandled exception. |
traceId | The traceId from the original HttpRequest. |
correlationId | The correlationId for the request. |
statusCode | The HttpResponse status code. |
If required, the correlation middleware call should go before the exception handler to ensure that any error logged will also include the correlation id
using Hackney.Core.Middleware.Exception;
namespace SomeApi
{
public class Startup
{
...
public static void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseCustomExceptionHandler();
...
}
}
}
The logging scope middleware sets up a logging scope for every incoming HTTP request. This means that every log statement made within that scope (i.e. during the HTTP request processing) will include an addition string that contains both the correlation id (and user email, if available in the headers) of the caller. This means that all other logging need not concern itself without having to add this data as it is already included.
When used in conjunction with the correlation middleware, the call to
UseLoggingScope()
should come after the call to UseCorrelationId()
.
using Hackney.Core.Middleware.Logging;
namespace SomeApi
{
public class Startup
{
...
public static void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseLoggingScope();
...
}
}
}
Project reference: Hackney.Core.DynamoDb
There is a ConfigureDynamoDB()
extension method provided to facilitate setting up an application to use AWS DynamoDb.
By calling it in the application startup, the following interfaces will be configured in the DI container:
IAmazonDynamoDB
IDynamoDBContext
By default it assumes there is an appropriate AWS profile configured where the application will run. See here for more details. This means that, at the very least, your application must have a region specified in its appsettings.json:
"AWS": {
"Region": "eu-west-2"
}
using Hackney.Core.DynamoDb;
namespace SomeApi
{
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
...
services.ConfigureDynamoDB();
...
}
}
}
If there is a local DynamoDb instance available then this can be used by amending the application settings as shown below:
"AWS": {
"Region": "DefaultRegion"
},
"DynamoDb": {
"LocalMode": true,
"LocalServiceUrl": "http://localhost:8000"
}
The LocalMode
flag must be set to true
and the LocalServiceUrl
should point to the local DynamoDb instance.
These 2 values can also be set as environment variables.
There are a number of different converters provided to make using the IDynamoDBContext
easier.
They are used against a property on a model class to tell the underlying AWS libraries how to marshal the property into and out of the database.
This should be used on bool
properties.
The default AWS bool
processing converts the bool value to an integer of 0 or 1, which may or may not be a problem in itself depending on your specific data requirements.
However if you need to filter your database query on a bool property, this attribute will need to be decorating it.
This will mean that the bool value will get serialsed into and out of the database properly (as "true" or "false") and it will also mean that the property can be used as needed in any filter.
[DynamoDBProperty(Converter = typeof(DynamoDbBoolConverter))]
public bool IsActive { get; set; }
This should be used on DateTime
properties.
The default AWS DateTime
processing is very restrictive, expects that value to be in a very specific format and will throw an exception if it is not in that format.
This converter will always store in the format yyyy-MM-ddTHH:mm:ss.fffffffZ
, but is much more forgiving when reading data out of the database.
[DynamoDBProperty(Converter = typeof(DynamoDbDateTimeConverter))]
public DateTime Created { get; set; }
This should be used on enum
properties.
It ensures that an enum value is stored in the database as the string representation of the value rather than the numeric value.
[DynamoDBProperty(Converter = typeof(DynamoDbEnumConverter<TargetType>))]
public TargetType TargetType { get; set; }
This should be used on properties that are a List<>
of enum
values.
Internally it operates in the same way as the DynamoDbEnumConverter
.
[DynamoDBProperty(Converter = typeof(DynamoDbEnumListConverter<PersonType>))]
public List<PersonType> PersonTypes { get; set; } = new List<PersonType>();
This should be used on properties that are a custom object.
The coverter works by simply serialising the object to and from the database using Json.
It does this because the native AWS functionality for nested objects is very simplistic and does not honour the LowerCamelCaseProperties
value set on the root class.
By simply converting the entire sub-object using Json we bypass these limitations.
However this will mean that any DynamoDb converters set against properties on nested objects will not be used.
[DynamoDBProperty(Converter = typeof(DynamoDbObjectConverter<AuthorDetails>))]
public AuthorDetails Author { get; set; }
This should be used on properties that are a lists of custom objects.
Internally it operates in the same way as the DynamoDbObjectConverter
.
[DynamoDBProperty(Converter = typeof(DynamoDbEnumListConverter<PersonType>))]
public List<PersonType> PersonTypes { get; set; } = new List<PersonType>();
The implementation of querying for paged results within DynamoDb is limited compared to other database technologies. In essence if you query a table with a specific page size, if there are potentially more results beyond that page size limit then a token will be returned along with the results. This token (it is a json object) indicates where in the full results list the current set of results ended. If this token is supplied in the next query then the list of results will start from where the last set finished.
This is a template class used to encapsulate the results of a paged query.
Property | Description |
---|---|
Results | The list of result objects from the query. |
PaginationDetails | A PaginationDetails object that may or may not contain a token. |
This class encapsulates the results of a paged query. It contains static methods to encode or decode a token value as needed.
Property | Description |
---|---|
HasNext | Boolen value indicating whether or not there is a next token. |
NextToken | The token returned by the call to the AWS SDK encoded as a Base64 string.null if there is no token. |
This is an extension method on the IDynamoDBContext
interface that is used to make a paged query against the database
and returns a PagedResult
object.
If there are no more results after the query has been made (regardless of the specified page size) then the
PaginationDetails.NextToken
will be null.
public async Task<PagedResult<NoteDb>> GetNotesByTargetIdAsync(GetNotesByTargetIdQuery query)
{
_logger.LogDebug($"Querying DynamoDB for notes for TargetId {query.TargetId}");
int pageSize = query.PageSize.HasValue ? query.PageSize.Value : MAX_RESULTS;
var queryConfig = new QueryOperationConfig
{
IndexName = GETNOTESBYTARGETIDINDEX,
BackwardSearch = true,
ConsistentRead = true,
Limit = pageSize,
PaginationToken = PaginationDetails.DecodeToken(query.PaginationToken),
Filter = new QueryFilter(TARGETID, QueryOperator.Equal, query.TargetId)
};
return await _dynamoDbContext.GetPagedQueryResultsAsync<NoteDb>(queryConfig).ConfigureAwait(false);
}
There is a DynamoDbHealthCheck
class implemented that uses the
Microsoft Health check framework.
The check verifies that the required DynamoDb table is accessible by performing a DescribeTable
call.
The template argument supplied to the AddDynamoDbHealthCheck()
call is the name of a database model class that has the DynamoDbTable
attribute applied to it. The method uses this attribute to determine the table name to use to query the database.
using Hackney.Core.DynamoDb.HealthCheck;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
namespace SomeApi
{
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
...
services.AddDynamoDbHealthCheck<NoteDb>();
...
}
public static void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseEndpoints(endpoints =>
{
endpoints.MapHealthChecks("/health");
});
...
}
}
}
Project reference: Hackney.Core.HealthCheck
The default HTTP response from the Microsoft Health check framework is simply a headline HealthStatus
value with the appropriate Http status code.
In order to provide more meaningful response information a custom response writer, the HealthCheckResponseWriter.WriteResponse
static method,
has been implemented to serialise the entire HealthReport
as json.
The only differences between the framework HealthReport
class and the serialised response are:
- Durations are given in milliseconds only
- Any exception object has been replaced with just the exception message.
using Hackney.Core.DynamoDb.HealthCheck;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
namespace SomeApi
{
public class Startup
{
public static void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseEndpoints(endpoints =>
{
endpoints.MapHealthChecks("/health", new HealthCheckOptions()
{
ResponseWriter = HealthCheckResponseWriter.WriteResponse
});
});
...
}
}
}
Project reference: Hackney.Core.JWT
The TokenFactory
implementation of the ITokenFactory
interface is designed to easily retrieve a JWT token sent in the headers of an Http request.
The ITokenFactory
interface is made available by using the AddTokenFactory()
extension method during your application start-up.
Usage
using Hackney.Core.JWT;
namespace SomeApi
{
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
...
services.AddTokenFactory();
...
}
}
}
Project reference: Hackney.Core.Logging
There is a helper method that can be used durung application startup to configure the generic Microsoft ILogger
framework to log to the AWS Lambda logger. By making use of the standard Microsoft implementation, it will also
make use of the any standard logging configuration
in the appsettings.json configuration file.
The ConfigureLambdaLogging()
extension method will set up logging so to use the Lambda logger
(as well as logging to debug output, and the console if the application is running in the development environment).
using Hackney.Core.Logging;
namespace SomeApi
{
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
...
services.ConfigureLambdaLogging(Configuration);
...
}
}
}
By making use of the Aspect Injector library it is possible to easily add method logging with a single line of code. This method logging generates simple log statements for the start and end of the decorated method. It does this by generating (at compile time) a proxy around the class and then calling a custom aspect before and after a decorated method. It is this custom aspect that will perform the logging.
In this way it is possible to easily add method logging without polluting methods with code that would have to be continually replicated.
Note:
Because the aspect proxy is generated at compile time, this will affect how unit tests are written.
Any unit tests that are on a class that uses the [LogCall]
attribute (regardless of whether or not they are testing
a decorated method) must also ensure that the DI container used by the aspect is configured appropriately.
The first call adds the necessary DI container registrations and the second call ensures that the DI
container used to inject the ILogger
into the custom aspect is the same one that is created in
the application startup.
using Hackney.Core.Middleware.Logging;
namespace SomeApi
{
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
...
services.AddLogCallAspect();
...
}
public static void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseLogCall();
...
}
}
}
To add method logging to a method simply decorate the method with the [LogCall]
attribute.
using Hackney.Core.Logging;
namespace SomeApi
{
public class SomeClass
{
// The default log level is Trace.
[LogCall]
public void SomeMethod()
{
...
}
// It is possible specify the log level required on the attribute.
[LogCall(LogLevel.Information)]
public void SomeOtherMethod()
{
...
}
}
}