ASP.NET Core

Home > .NET > .NET Core

Introduction

For all the topics below, check this github repository: Playground to see an example of implementation.

Table of Contents

  1. Migration from Web Api 2 to .NET Core 2
  2. Inheritance
  3. Exception Management
  4. AutoMapper
  5. Unit tests
  6. Pagination and hyperlinks
  7. Tips

Migration from Web Api 2 to DotNet Core 2

Before migrating to .Net Core, you need to check that all nuget packages and libraries used in the project are availale in .Net standard. Also, some features are not availale in .Net Core, for example message security in wcf, see details here.

Here are te steps I followed to migrate an Api from a Web Api 2 to .Net Core 2 project.

Migrate dependencies to dotnet standard

First, all dependencies must be migrated from .Net Framework 4.6.X to .Net Standard 2.0. Make sure that the sdk and runtime are installed. They are available here.

Two ways to migrate:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <AssemblyName>[YourAssemblyName]</AssemblyName>
    <RootNamespace>[YourRootNamespace]</RootNamespace>
  </PropertyGroup>
</Project>

Then add nuget packages. Nuget packages will be added under <PropertyGroup> section, in <ItemGroup>. Example:

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Logging" Version="2.0.0" />
    <PackageReference Include="MongoDB.Bson" Version="2.5.0" />
    <PackageReference Include="X.PagedList" Version="7.2.2" />
  </ItemGroup>

Create Api project

Create a new project that will replace the current web api.

Config files and logging

Configuration is available in appsettings.json file. Copy from old Web.config file to appsettings.json file. In this example, I have one connection string, two app settings, and I use common.logging with log4net. In .Net Core, I switched to the default logging Microsoft.Extensions.Logging with NLog, using a nlog.config file in the same level as appsettings.json file. Packages: NLog and NLog.Web.AspNetCore.

Before:

<configSections>
    <sectionGroup name="common">
      <section name="logging" type="Common.Logging.ConfigurationSectionHandler, Common.Logging"/>
    </sectionGroup>
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/>
  </configSections>
<appSettings>
    <add key="Key1" value="Value1" />
    <add key="Key2" value="Value2" />
</appSettings>
<connectionStrings>
    <add name="Name1"
         connectionString="Connection1" />
</connectionStrings>
<log4net configSource="...."/>

After:

{
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Trace",
      "System": "Warning",
      "Microsoft": "Warning"
    }
  },
  "Key1": "Value1",
  "Key2": "Value2",
  "Name1": "Connection1"
}

To access configuration, there is no more ConfigurationManager. You can access it from Startup.cs file. Example: var appSettingsValue = Configuration[AppSettingsKey];.

To configure NLog with Microsoft.Extensions.Logging, update the Program.cs .

public static void Main(string[] args)
{
    // NLog: setup the logger first to catch all errors
    var logger = LogManager.LoadConfiguration("nlog.config").GetCurrentClassLogger();
    try
    {
        logger.Debug("init main");
        BuildWebHost(args).Run();
    }
    catch (Exception ex)
    {
        //NLog: catch setup errors
        logger.Error(ex, "Stopped program because of exception");
        throw;
    }
}

public static IWebHost BuildWebHost(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .ConfigureLogging((hostingContext, logging) =>
        {
            logging.ClearProviders();
            logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
        })
        .UseNLog()  // NLog: setup NLog for Dependency injection
        .Build();
}

Example of nlog.config file. Make sure that it is copied to output directory (PreserveNewest or Always):

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true"
      internalLogLevel="Warn"
      internalLogFile="D:\MyApi\Logs\internal-nlog.log">
    <!-- the targets to write to -->
    <targets>
        <!-- write logs to file -->
        <target xsi:type="File" name="allfile" fileName="D:\MyApi\Logs\All-${shortdate}.log"
                layout="${date:universalTime=True:format=yyyy-MM-ddTHH\:mm\:ss.fff}|${uppercase:${level}}|${logger}|${message} ${exception}" />
        <!-- another file log, only own logs. Uses some ASP.NET core renderers -->
        <target xsi:type="File" name="ownFile" fileName="D:\MyApi\Logs\MyApi-${shortdate}.log"
                layout="${date:universalTime=True:format=yyyy-MM-ddTHH\:mm\:ss.fff}|${uppercase:${level}}|${logger}|${message} ${exception}" />
    </targets>
    <!-- rules to map from logger name to target -->
    <rules>
        <!-- Ignore trace Microsoft logs in allFile -->
        <logger name="Microsoft.*" maxlevel="Trace" final="true" />
        <logger name="*" minlevel="Trace" writeTo="allfile" />
        
        <!-- Ignore all Microsoft logs in ownFile -->
        <logger name="Microsoft.*" final="true" />
        <logger name="*" minlevel="Trace" writeTo="ownFile" />
    </rules>
</nlog>

Then, we can inject loggers, for example ILogger<MyService>. More details here.

Dependency injection

There is no need to use Unity for dependency injection. We can use the provided one. Example: services.AddTransient<IFooRepository, FooRepository>(c => new FooRepository(myconnectionString));.

Swagger and Api Versioning

Here is an example of what we could have in Web Api 2:

// Add Versioning and versioned documentation using swagger
config.AddApiVersioning(
        o =>
        {
            o.AssumeDefaultVersionWhenUnspecified = true;
            o.DefaultApiVersion = new ApiVersion(1, 0);
            o.ReportApiVersions = true;
        }
    );
var apiExplorer = config.AddVersionedApiExplorer(o => o.GroupNameFormat = "F");
var virtualPath = HostingEnvironment.ApplicationHost.GetVirtualPath();
config.EnableSwagger(
    SwaggerRootTemplate,
    swagger =>
    {
        swagger.RootUrl(req => req.RequestUri.GetLeftPart(UriPartial.Authority) + req.GetConfiguration().VirtualPathRoot.TrimEnd('/') + virtualPath);
        swagger.IncludeXmlComments(XmlCommentsPath);
        swagger.MultipleApiVersions(
            (apiDescription, version) => apiDescription.GetGroupName() == version,
            info =>
            {
                foreach (var group in apiExplorer.ApiDescriptions)
                {
                    info.Version(group.Name, $"My API {group.ApiVersion}");
                }
            });
    })
    .EnableSwaggerUi(swagger =>
    {
        swagger.EnableDiscoveryUrlSelector();
        swagger.DisableValidator();
    });

In .Net core, the syntax is different and is done in two steps after installing nuget packages: Microsoft.AspNetCore.Mvc.Versioning, Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer and Swashbuckle.AspNetCore and generating documentation files for Debug and Release:

// Add Versioning and versioned documentation using swagger
services.AddMvcCore().AddVersionedApiExplorer(o =>
{
    o.GroupNameFormat = "F";
    // note: this option is only necessary when versioning by url segment. the SubstitutionFormat
    // can also be used to control the format of the API version in route templates
    o.SubstituteApiVersionInUrl = true;
});
services.AddMvc();

services.AddApiVersioning(
    o =>
    {
        o.AssumeDefaultVersionWhenUnspecified = true;
        o.DefaultApiVersion = new ApiVersion(1, 0);
        o.ReportApiVersions = true;
    });

// Register the Swagger generator, defining one or more Swagger documents
services.AddSwaggerGen(c =>
{
    var provider = services.BuildServiceProvider().GetRequiredService<IApiVersionDescriptionProvider>();

    foreach (var description in provider.ApiVersionDescriptions)
    {
        c.SwaggerDoc(description.GroupName, new Info()
            {
                Title = $"My API {description.ApiVersion}",
                Version = description.ApiVersion.ToString()
            });
    }

    // Set the comments path for the Swagger JSON and UI.
    var basePath = AppContext.BaseDirectory;
    c.IncludeXmlComments(Path.Combine(basePath, XmlComments));
});
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();

// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
    foreach (var description in provider.ApiVersionDescriptions)
    {
        c.SwaggerEndpoint($"{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant());
    }
});
private ApiVersion RequestedApiVersion => HttpContext.ApiVersionProperties()?.ApiVersion;

Fluent validation

Registering fluent validation is different between Web Api 2 and .Net core. In Web Api 2:

var validators = AssemblyScanner.FindValidatorsInAssemblyContaining<MyValidator>();
validators.ForEach(validator => unityContainer.RegisterType(validator.InterfaceType, validator.ValidatorType, new HierarchicalLifetimeManager()));
FluentValidationModelValidatorProvider.Configure(config, provider =>
{
    provider.ValidatorFactory = new UnityValidatorFactory(unityContainer);
});

In .Net Core:

services.AddMvc().AddFluentValidation(fv =>
{
    fv.RegisterValidatorsFromAssemblyContaining<MyValidator>();
    fv.ImplicitlyValidateChildProperties = true;
}
);

To return a bad request when the validation fails, create a ValidateCommand attribute and add it to the operations you want to validate:


    /// <summary>
    /// Validates model state before executing the method.
    /// </summary>
    public class ValidateCommandAttribute : ActionFilterAttribute
    {

        /// <summary>
        /// Occurs before the action method is invoked.
        /// </summary>
        /// <param name="actionContext"> The action context. </param>
        public override void OnActionExecuting(ActionExecutingContext actionContext)
        {
            if (!actionContext.ModelState.IsValid)
            {
                var controller = actionContext.Controller as ControllerBase;
                if (controller != null)
                {
                    actionContext.Result = controller.BadRequest(actionContext.ModelState);
                    return;
                }
            }
            base.OnActionExecuting(actionContext);
        }
    }

Custom model binding

Model Binder

Example for a custom binding, used to bind comma separated values to a list of strings.

public class MyListBinder : IModelBinder
{
    public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
    {
        var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        var result = new List<string>();
        if (!string.IsNullOrEmpty(value?.AttemptedValue))
        {
            var values = value.AttemptedValue.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries);
            result.AddRange(values);
        }
        bindingContext.Model = result;

        return true;
    }
}

And it is declared in WebApiConfig.cs file: config.BindParameter(typeof(IList<string>), new MyListBinder());

public class MyListBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
            throw new ArgumentNullException(nameof(bindingContext));

        var modelName = bindingContext.ModelName;
        var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);

        if (valueProviderResult == ValueProviderResult.None || valueProviderResult.Length == 0)
            return Task.CompletedTask;

        var model = valueProviderResult.Values
            .SelectMany(x => x?.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries))
            .Where(y => !string.IsNullOrEmpty(y))
            .Distinct()
            .ToList();

        bindingContext.Result = ModelBindingResult.Success(model);

        return Task.CompletedTask;
    }
}

public class MyListBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null)
            throw new ArgumentNullException(nameof(context));

        if (context.Metadata.ModelType == typeof(IList<string>))
            return new MyListBinder();

        return null;
    }
}

And it is declared in Startup.cs file: services.AddMvc(options => { options.ModelBinderProviders.Insert(0, new MyListBinderProvider());});

Input formatter

Example for a custom parsing of request’s body.

 public class MyCustomBodyModelBinder : HttpParameterBinding
    {
        public MyCustomBodyModelBinder(HttpParameterDescriptor descriptor) : base(descriptor)
        {
        }

        public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken)
        {
            var binding = actionContext.ActionDescriptor.ActionBinding;

            var content = actionContext.Request.Content;

            return content.ReadAsStringAsync().ContinueWith(task =>
            {
                var json = task.Result;
                var bindingParameter = binding.ParameterBindings.OfType<MyCustomBodyModelBinder>().FirstOrDefault();
                if (bindingParameter != null)
                {
                    var type = bindingParameter.Descriptor.ParameterType;
                    var name = bindingParameter.Descriptor.ParameterName;
                    var converted = CustomConvert(json, type);

                    SetValue(actionContext, converted);
                    var modelMetadataProvider = Descriptor.Configuration.Services.GetModelMetadataProvider();
                    var validator = Descriptor.Configuration.Services.GetBodyModelValidator();
                    validator.Validate(converted, type, modelMetadataProvider, actionContext, name);                    
                }
            });
        }

        public override bool WillReadBody => true;
    }
}

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Parameter, Inherited = true, AllowMultiple = false)]
public sealed class FromMyCustomBodyAttribute : ParameterBindingAttribute
{
    public override HttpParameterBinding GetBinding(HttpParameterDescriptor parameter)
    {
        if (parameter == null)
            throw new ArgumentNullException(nameof(parameter));

        return new MyCustomBodyModelBinder(parameter);
    }
}
public class MyCustomInputFormatter : InputFormatter
{
    public MyCustomInputFormatter()
    {
        SupportedMediaTypes.Add("application/json");
    }
    public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
    {
        var request = context.HttpContext.Request;
        using (var reader = new StreamReader(request.Body))
        {
            var content = await reader.ReadToEndAsync();
            var type = context.ModelType;
            var converted = CustomConvert(content, type);
            return await InputFormatterResult.SuccessAsync(converted);
        }
    }
    protected override bool CanReadType(Type type)
    {
        return type.Assembly == typeof(MyType).Assembly;
    }
}

CORS

In Web Api, to enable CORS fo everyone: config.EnableCors(new EnableCorsAttribute("*", "*", "*"));. In .Net Core, it is done in two parts: services.AddCors(); and app.UseCors(builder => builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());.

Middlewares

Exception handling

Example in WebApi 2, an exception filter is declared: config.Filters.Add(new GlobalExceptionFilter());

public class GlobalExceptionFilter : ExceptionFilterAttribute
{
    public override void OnException(HttpActionExecutedContext context)
    {
        var exception = context.Exception as ApiException;
        var httpError = new HttpError(...) {....};
        var statusCode = ....;
        context.Response = context.Request.CreateErrorResponse(statusCode, httpError);
    }
}

In .Net Core, a middleware is declared: app.UseMiddleware(typeof(ErrorHandlingMiddleware));

public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate _next;

    public ErrorHandlingMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
       var result = JsonConvert.SerializeObject(new { Error = exception.Message, ... });
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = ...;
        return context.Response.WriteAsync(result);
    }
}

Logging

public class LoggingMessageHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var requestUri = request.RequestUri.ToString();
        var stopwatch = Stopwatch.StartNew();
        string responseContent = null;
        string statusCode = null;
        string statusReason = null;

        try
        {
            var response = await base.SendAsync(request, cancellationToken);

            responseContent = response.Content == null ? null : await response.Content.ReadAsStringAsync();

            statusCode = ((int)response.StatusCode).ToString();
            statusReason = response.ReasonPhrase;

            return response;
        }
        finally
        {
            stopwatch.Stop();
            var elapsed = stopwatch.Elapsed.ToString();
            var requestContent = request.Content == null ? null : await request.Content.ReadAsStringAsync();
            //Log here
        }
    }
}

public class LoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;

    public LoggingMiddleware(RequestDelegate next, ILogger<LoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task Invoke(HttpContext context)
    {
        var request = context.Request;
        var requestUri = request.GetDisplayUrl();
        var stopwatch = Stopwatch.StartNew();
        string statusCode = null;

        try
        {
            await _next(context);
            var response = context.Response;
            statusCode = response.StatusCode.ToString();
        }
        finally
        {
            stopwatch.Stop();
            _logger.LogDebug($"RequestMethod={request.Method};RequestUri={requestUri};ResponseCode={statusCode};ElapsedTime={stopwatch.Elapsed}");
        }
    }
}

Changes in controller

Inheritance

Return derived classes

Let’s start with this basic example: we want to get a list of vehicles.


        [HttpGet]
        [ProducesResponseType(typeof(List<Vehicle>), 200)]
        [Route("", Name = RouteNameSearch)]
        public async Task<IActionResult> GetVehiclesAsync()

This will return a list of objects having only the properties declared in Vehicle class. To be able to get properties declared in sub-classes, the Vehicle class needs to know its inherited classes:


    [KnownType(typeof(Bike))]
    [KnownType(typeof(Car))]
    public class Vehicle

Add derived classes in input

Example: we want to post a vehicle.


        [HttpPost]
        [ValidateCommand]
        [ProducesResponseType(typeof(Vehicle), 201)]
        [Route("")]
        public async Task<IActionResult> Post([FromBody] Vehicle vehicle)

To be able to accept a derived class, we need to have a custom json formatter and declare it in Startup.cs file.


    /// <summary>
    /// Converter used to parse a vehicle.
    /// </summary>
    public class VehicleConverter : JsonConverter
    {
        /// <summary>
        /// Determines whether this instance can convert the specified object type.
        /// </summary>
        public override bool CanConvert(Type objectType)
        {
            return typeof(Vehicle).GetTypeInfo().IsAssignableFrom(objectType);
        }

        /// <summary>
        /// Gets a value indicating whether this Newtonsoft.Json.JsonConverter can write.
        /// </summary>
        public override bool CanWrite => false;

        /// <summary>
        /// Reads the JSON representation of the object.
        /// </summary>
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            if (reader.TokenType == JsonToken.Null)
                return null;

            var item = JObject.Load(reader);
            var vehicleType = item["VehicleType"]?.ToString(); // Here, we assume that vehicle has a property called VehicleType containing vehicle type.
            switch (vehicleType)
            {
                case "Bike":
                    return item.ToObject<Bike>();
                case "Car":
                    return item.ToObject<Car>();
                default:
                    throw new ArgumentException($"Unknown vehicle type '{vehicleType}'");
            }
        }

        /// <summary>
        /// Writes the JSON representation of the object.
        /// </summary>
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            //Not used because CanWrite is set to false
        }
    }



    public class BodyInheritanceInputFormatter : InputFormatter
    {
        public BodyInheritanceInputFormatter()
        {
            SupportedMediaTypes.Add("application/json");
        }
        public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
        {
            var request = context.HttpContext.Request;
            using (var reader = new StreamReader(request.Body))
            {
                var content = await reader.ReadToEndAsync();
                var type = context.ModelType;

                var converters = new JsonConverter[] { new VehicleConverter() }; // Add other converters here

                var converted = JsonConvert.DeserializeObject(content, type, converters);
                return await InputFormatterResult.SuccessAsync(converted);
            }
        }
        protected override bool CanReadType(Type type)
        {
            return type.Assembly == typeof(Vehicle).Assembly;
        }
    }


            services.AddMvc(options =>
            {
                options.InputFormatters.Insert(0, new BodyInheritanceInputFormatter()); // Add custom formatter to parse body into derived class
            })
            .AddJsonOptions(options => options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore) // Ignore null values in response

Add derived classes in documentation

To include them in the swagger documentation, add this swagger gen option: c.DocumentFilter<PolymorphismDocumentFilter>(); and define a new filter:


public class PolymorphismDocumentFilter : IDocumentFilter
{
    private static void RegisterSubClasses(ISchemaRegistry schemaRegistry)
    {
        var assembly = typeof(Vehicle).Assembly;
        var allTypes = assembly.GetTypes();

        var allBaseClassDtoTypes = allTypes
                                    .Where(x => x.IsDefined(typeof(KnownTypeAttribute), false))
                                    .ToList();

        var derivedTypes = allTypes
            .Where(x => allBaseClassDtoTypes.Any(abstractType => abstractType != x && abstractType.IsAssignableFrom(x)))
            .ToList();

        foreach (var item in derivedTypes)
        {
            schemaRegistry.GetOrRegister(item);
        }
    }

    public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
    {
        RegisterSubClasses(context.SchemaRegistry);
    }
}

Validate derived classes

To validate derived classes with fluent validation, first, create a base class for validators, that includes a method that can be used to add validation rules for derived classes:


public class ValidatorBase<TBase> : AbstractValidator<TBase>
{
    public void MapDerivedValidator<TType, TValidatorType>() where TValidatorType : IEnumerable<IValidationRule>, IValidator<TType>, new() where TType : TBase
    {
        When(t => t.GetType() == typeof(TType), () => AddDerivedRules<TValidatorType>());
    }

    private void AddDerivedRules<T>() where T : IEnumerable<IValidationRule>, new()
    {
        IEnumerable<IValidationRule> validator = new T();
        foreach (var rule in validator)
        {
            AddRule(rule);
        }
    }
}

Then, in the validator for the parent class, include validator for sub-classes.


public class VehicleValidator : ValidatorBase<Vehicle>
{
    public VehicleValidator()
    {
        RuleFor(request => request.VehicleType)
            .NotEmpty();

        // Add rules for common properties here

        MapDerivedValidator<Bike, BikeValidator>();
    }
}

public class BikeValidator : ValidatorBase<Bike>
{
    public BikeValidator()
    {
        // Add rules for properties specific to a bike
    }
}

Exception Management

To share api error codes with external applications (clients) instead of error messages, create an enumeration with all the error codes and define exceptions using these error codes. Here is a dummy example:


public enum ApiErrorCode
{
    InternalError,
    UserNotFound,
    UserAlreadyExists,
    UserWrongPassword,
    UserDisabled
}

public class ApiException : Exception
{
    public ApiErrorCode ApiErrorCode { get; set; }

    public ApiException(ApiErrorCode errorCode, string message, Exception innerException = null)
        : base(message, innerException)
    {
        ApiErrorCode = errorCode;
    }
}

You can also create other exceptions inheriting from ApiException and that will be used to return an accurate http error code. Here, for example, we create two classes: ValidationApiException and ResourceNotFoundApiException. The ValidationApiException will be thrown when data in the input is invalid, and ResourceNotFoundApiException when the resource we are looking for is not found.


public class ValidationApiException : ApiException
{
    public ValidationApiException(ApiErrorCode errorCode, string message, Exception innerException = null)
        : base(errorCode, message, innerException)
    {
    }
}

Example of use:


[HttpGet]
[ProducesResponseType(typeof(UserDto), 200)]
[Route("{id}", Name = RouteNameGetById)]
public async Task<IActionResult> GetAsync(string id)
{
    if (string.IsNullOrEmpty(id))
        throw new ValidationApiException(ApiErrorCode.MissingInformation, $"Parameter {nameof(id)} must be provided.");

    var user = await _userService.GetUserByIdAsync(id);

    if (user == null)
        throw new ResourceNotFoundApiException(ApiErrorCode.UserNotFound, $"Cannot find user with id=\"{id}\"");

    var userDto = Mapper.Map<UserDto>(user);

    return Ok(userDto);
}

Then, define a middleware for exception handling. Here, the exception is logged, and the http status code is determined by the exception type. The ApiErrorCode is returned in the response.


public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;

    public ErrorHandlingMiddleware(RequestDelegate next, ILogger<ErrorHandlingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        var code = HttpStatusCode.InternalServerError; // 500 if unexpected
        _logger.LogWarning(exception.Message);
        _logger.LogTrace($"Stacktrace: {exception.StackTrace}");
        while (exception.InnerException != null)
        {
            exception = exception.InnerException;
            _logger.LogWarning($"Inner exception: {exception.Message}");
            _logger.LogTrace($"Stacktrace: {exception.StackTrace}");
        }

        var apiException = exception as ApiException;
        if (apiException != null)
            code = GetHttpStatusCodeFromException(apiException);

        var apiErrorCode = apiException?.ApiErrorCode ?? ApiErrorCode.InternalError;

        var result = JsonConvert.SerializeObject(new { Error = exception.Message, ApiErrorCode = apiErrorCode.ToString() });
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)code;
        return context.Response.WriteAsync(result);
    }

    private HttpStatusCode GetHttpStatusCodeFromException(ApiException exception)
    {
        if (exception is ResourceNotFoundApiException)
            return HttpStatusCode.NotFound;

        if (exception is ValidationApiException)
            return HttpStatusCode.BadRequest;

        // Add here other exceptions

        return HttpStatusCode.InternalServerError;
    }
}

Finally, use this middleware in Startup.cs file.

app.UseMiddleware(typeof(ErrorHandlingMiddleware));

AutoMapper

To map objects in dotnet core, we can still use AutoMapper.


public class MyProfile : Profile
{
    public override string ProfileName => nameof(MyProfile);

    public MyProfile()
    {
        CreateMap<Item, ItemDto>();
    }
}


public class AutoMapperConfig
{
    public static IMapper Configure()
    {
        var config = new MapperConfiguration(x =>
        {
            x.AddProfile(new MyProfile());
            x.AllowNullCollections = true;
        });

        var mapper = config.CreateMapper();
        mapper.ConfigurationProvider.AssertConfigurationIsValid();

        return mapper;
    }
}


var mapper = AutoMapperConfig.Configure();
services.AddTransient<IMapper, IMapper>(c => mapper);

public MyController(MyService myService, IMapper mapper, ILogger<MyController> logger)
{
    _myService = myService;
    _mapper = mapper;
    _logger = logger;
}

[HttpGet]
[ProducesResponseType(typeof(List<ItemDto>), 200)]
public List<ItemDto> Get()
{
    var items = _itemsService.GetAllItems();
    return _mapper.Map<List<ItemDto>>(items);
}

Unit tests

Example 1: using AutoFixture, xunit and moq

In these unit tests, I use AutoFixture, xunit and moq.

Example with xunit and AutoFixture:

[Fact]
public void MyTest()
{
    // Arrange
    var fixture = new Fixture();
    var expectedItem = fixture.Create<MyClass>();
    // Additional arrange stuff

    // Act
    // Call operation here

    // Assert
    // Add assertions here
}

To inject data in a theory using autofixture, we need AutoFixture.Xunit2 nuget package.

[Theory, AutoData]
public void MyTest(MyClass expectedItem)
{
    // Arrange
    // Additional arrange stuff

    // Act
    // Call operation here

    // Assert
    // Add assertions here
}

AutoFixture Customization

For several object types, the initialization fails with an ObjectCreationExceptionWithPath exception. In this case, we need to customize AutoFixture.

For example, to customize the initialization of MongoDB.Bson.ObjectId type:


internal class AutoFixtureConventions : CompositeCustomization
{
    public AutoFixtureConventions()
        : base(new MongoObjectIdCustomization())
    {
    }

    private class MongoObjectIdCustomization : ICustomization
    {
        public void Customize(IFixture fixture)
        {
            fixture.Register(ObjectId.GenerateNewId);
        }
    }
}

To be used with Fixture initialization:

var fixture = new Fixture().Customize(new AutoFixtureConventions());

Or to be used with a custom AutoData attribute.

public class CustomAutoDataAttribute : AutoDataAttribute
{
    public CustomAutoDataAttribute()
        : base(() => new Fixture().Customize(new AutoFixtureConventions()))
    {
    }
}

Integrating moq

Example without AutoFixture, with a service mocking a call to a repository.

[Fact]
public async Task GetByIdReturnsExpectedItem()
{
    // Arrange
    var itemsRepositoryMock = new Mock<IItemsRepository>();
    var itemsService = new ItemsService(itemsRepositoryMock.Object);
    var expectedItem = new Item();
    var id = ObjectId.GenerateNewId();
    itemsRepositoryMock.Setup(x => x.GetById(id)).ReturnsAsync(expectedItem);

    // Act
    var result = await itemsService.GetById(id.ToString());

    // Assert
    Assert.Equal(result, expectedItem);
    itemsRepositoryMock.VerifyAll();
}

Example autodata, needs the package AutoFixture.AutoMoq, and registering new AutoMoqCustomization():

internal class AutoFixtureConventions : CompositeCustomization
{
    public AutoFixtureConventions()
        : base(new MongoObjectIdCustomization(), new AutoMoqCustomization())
    {
    }

    private class MongoObjectIdCustomization : ICustomization
    {
        public void Customize(IFixture fixture)
        {
            fixture.Register(ObjectId.GenerateNewId);
        }
    }
}
[Theory, CustomAutoData]
public async Task GetByIdReturnsExpectedItem(Item expectedItem, ObjectId id,  [Frozen]Mock<IItemsRepository> itemsRepositoryMock, ItemsService itemsService)
{
    // Arrange
    itemsRepositoryMock.Setup(x => x.GetById(id)).ReturnsAsync(expectedItem);

    // Act
    var result = await itemsService.GetById(id.ToString());

    // Assert
    Assert.Equal(result, expectedItem);
    itemsRepositoryMock.VerifyAll();
}

Example 2: using NUNit and Bogus

In this unit test, I use NUNit and Bogus.

[TestFixture]
public class ItemDtoValidatorTest
{
	[TestCase("")]
	[TestCase(null)]
	public void ValidateItemDto_ReturnsError_WhenNameIsNotProvided(string name)
	{
		// Arrange
		var validator = new ItemDtoValidator();
		var testItems = new Faker<ItemDto>()
			.RuleFor(x => x.Name, f => name)
			.RuleFor(x => x.Id, f => Guid.NewGuid().ToString())
			.RuleFor(x => x.Description, f => f.Lorem.Paragraph())
			.RuleFor(x => x.Owner, f => f.Name.FindName())
			.RuleFor(x => x.Tags, f => f.Lorem.Words().ToList());

		var item = testItems.Generate();

		// Act
		var result = validator.Validate(item);

		// Assert
		Assert.That(result.Errors.Count, Is.EqualTo(1));
		Assert.That(result.Errors.Any(x => x.ErrorMessage.Contains(nameof(item.Name))), Is.True);
	}
}

For pagination, I use X.PagedList nuget package.

Adapt repository for pagination

Extension to help creating a paged list:

public static class PaginationExtensions
{
    public static IPagedList<T> ToPagedList<T>(this IEnumerable<T> list, int skip, int limit, int totalCount)
    {
        var pageNumber = (skip / limit) + 1;
        return new StaticPagedList<T>(list, pageNumber, limit, totalCount);
    }
}

Usage example for MongoDb driver:

var query = collection.Find(filter)
    .SortBy(acc => acc.Id)
    .Skip(searchParameters.Skip)
    .Limit(searchParameters.Limit);

var items = await query.ToListAsync();
var totalRows = (int)await collection.CountAsync(filter);
return items.ToPagedList(searchParameters.Skip, searchParameters.Limit, totalRows);

Adapt api for pagination

Create a PagedListDto class with the properties corresponding to IPagedList class:

public class PagedListDto<T> : ResourceBase
{
    public IList<T> Items { get; set; }
    public int FirstItemOnPage { get; set; }
    public bool HasNextPage { get; set; }
    public bool HasPreviousPage { get; set; }
    public bool IsFirstPage { get; set; }
    public bool IsLastPage { get; set; }
    public int LastItemOnPage { get; set; }
    public int PageCount { get; set; }
    public int PageNumber { get; set; }
    public int PageSize { get; set; }
    public int TotalItemCount { get; set; }
}

Create a mapping converter:

public class PagedListToDtoConverter<T1, T2> : ITypeConverter<IPagedList<T1>, PagedListDto<T2>>
{
    public PagedListDto<T2> Convert(IPagedList<T1> source, PagedListDto<T2> destination, ResolutionContext context)
    {
        var items = context.Mapper.Map<List<T2>>(source);

        return new PagedListDto<T2>
        {
            Items = items,
            FirstItemOnPage = source.FirstItemOnPage,
            HasNextPage = source.HasNextPage,
            HasPreviousPage = source.HasPreviousPage,
            IsFirstPage = source.IsFirstPage,
            IsLastPage = source.IsLastPage,
            LastItemOnPage = source.LastItemOnPage,
            PageCount = source.PageCount,
            PageNumber = source.PageNumber,
            PageSize = source.PageSize,
            TotalItemCount = source.TotalItemCount
        };
    }
}

To be added to the automapper profile: CreateMap(typeof(IPagedList<>), typeof(PagedListDto<>)).ConvertUsing(typeof(PagedListToDtoConverter<,>));

Change controller’s action to return the paged list instead of just a list of items:

[HttpGet]
[ProducesResponseType(typeof(PagedListDto<ItemDto>), 200)]
public async Task<IActionResult> Get(ItemSearchParameter search)
{
    var items = await _itemsService.GetItems(search);
    var itemDtos = _mapper.Map<PagedListDto<ItemDto>>(items);
    return Ok(itemDtos);
}

Extract skip and limit to a base class:

public class SearchBase
{
    private const int DefaultLimit = 100;

    private int? _skip;

    public int Skip
    {
        get { return _skip.GetValueOrDefault(0); }
        set { _skip = value; }
    }

    private int? _limit;

    public int Limit
    {
        get { return _limit.GetValueOrDefault(DefaultLimit); }
        set { _limit = value; }
    }
}

Create a resource base class, and make PagedListDto inherit from this class:

public abstract class ResourceBase
{
    public const string RelationNameSelf = "self";
    public const string RelationNamePrevious = "previous";
    public const string RelationNameNext = "next";
    public Dictionary<string, string> Links { get; set; }
    protected ResourceBase()
    {
        Links = new Dictionary<string, string>();
    }
}

Add extension class to build navigation links for paged list.

public static class PagedListExtensions
{
    public static void BuildNavigationLinks<T>(this PagedListDto<T> pagedList, Uri currentUri)
    {
        pagedList.Links[ResourceBase.RelationNameSelf] = currentUri.AbsoluteUri;
        var queryString = HttpUtility.ParseQueryString(currentUri.Query);
        SearchBase searchParam;
        var skipParameterName = nameof(searchParam.Skip);

        if (pagedList.HasNextPage)
        {
            var nbElementsToSkip = pagedList.LastItemOnPage;
            queryString.Set(skipParameterName, nbElementsToSkip.ToString());
            var newUri = new UriBuilder(currentUri) { Query = queryString.ToString() }.Uri;
            pagedList.Links[ResourceBase.RelationNameNext] = newUri.AbsoluteUri;
        }

        if (pagedList.HasPreviousPage)
        {
            var nbElementsToSkip = pagedList.FirstItemOnPage - 1 - pagedList.PageSize;
            queryString.Set(skipParameterName, nbElementsToSkip.ToString());
            var newUri = new UriBuilder(currentUri) { Query = queryString.ToString() }.Uri;
            pagedList.Links[ResourceBase.RelationNamePrevious] = newUri.AbsoluteUri;
        }
    }

    public static void BuildNavigationLinks<T>(this PagedListDto<T> pagedList, string currentUri)
    {
        pagedList.BuildNavigationLinks(new Uri(currentUri));
    }
}

Call it from controller’s action.

[HttpGet]
[ProducesResponseType(typeof(PagedListDto<ItemDto>), 200)]
public async Task<IActionResult> Get(ItemSearchParameter search)
{
    var items = await _itemsService.GetItems(search);
    var itemDtos = _mapper.Map<PagedListDto<ItemDto>>(items);
    itemDtos.BuildNavigationLinks(Request.GetDisplayUrl());
    return Ok(itemDtos);
}

To add hyperlinks in other resources, it is possible to inject them in AutoMapper.

Example of what is expected:

{
    "id": "5b130dd11469c43c08949da8",
    "name": "name 1",
    "description": "my description, etc",
    "owner": "me",
    "tags": [
    "tag1",
    "tag2"
    ],
    "links": {
    "operations": "http://localhost/PlaygroundApi/api/Operations?EntityId=5b130dd11469c43c08949da8&Skip=0&Limit=100",
    "self": "http://localhost/PlaygroundApi/api/Items/5b130dd11469c43c08949da8"
    }
}

In Startup.cs file, inject UrlHelper.

services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
services.AddScoped<IUrlHelper>(factory =>
{
    var actionContext = factory.GetService<IActionContextAccessor>().ActionContext;
    return new UrlHelper(actionContext);
});

var serviceProvider = services.BuildServiceProvider();
var mapper = AutoMapperConfig.Configure(serviceProvider);

In AutoMapperConfig and in the profile, inject IServiceProvider.

Use it in the mapping, to create the links. Example:

CreateMap<Item, ItemDto>()
    .ForMember(dest => dest.Links, opt => opt.Ignore())
    .AfterMap((src, dest) =>
    {
        if (dest.Id != null)
        {
            var urlHelper = (IUrlHelper)serviceProvider.GetService(typeof(IUrlHelper));
            dest.Links[ResourceBase.RelationNameOperations] = urlHelper.Link(OperationsController.RouteNameGetAsync, new OperationSearchParameter { EntityId = dest.Id });
            dest.Links[ResourceBase.RelationNameSelf] = urlHelper.Link(ItemsController.RouteNameGetById, new { id = dest.Id });
        }
    });

Tips

Ignore null values

To suppress null values from Api response, change serializer settings:

services.AddMvc().AddJsonOptions(options => options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore);

Get api versions

To get information about api versions from the controller, inject IApiVersionDescriptionProvider and get ApiVersionDescriptions.

Example:

[HttpGet]
[ProducesResponseType(typeof(VersionDto), 200)]
public IActionResult Get()
{
    var assemblyVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString();
    var apiDescriptions = _apiVersionDescriptionProvider.ApiVersionDescriptions;
    var versionDto = new VersionDto
    {
        AssemblyVersion = assemblyVersion,
        SupportedApiVersions = _mapper.Map<List<VersionDescriptionDto>>(apiDescriptions)
    };

    return Ok(versionDto);
}

Example of response:

{
  "assemblyVersion": "1.0.0.0",
  "supportedApiVersions": [
    {
      "version": "1.0-alpha1",
      "isDeprecated": true
    },
    {
      "version": "1.0-alpha2",
      "isDeprecated": false
    },
    {
      "version": "1.0",
      "isDeprecated": false
    }
  ]
}