J Lewis
J Lewis

Reputation: 522

Passing DateTimeOffset as Route Attribute

I've looked at this question about DateTimeOffset query parameters, but I explicitly want to be able to pass a DateTimeOffset as a route attribute, not a query parameter (will likely need further routing after the date). In addition I want a route that does not include the date, like so:

[Route("api/Controller/Action/")]
[HttpGet]
public async Task<ActionResult> ControllerAction() 
{
    //blah
}

[Route("api/Controller/Action/{dateParam:DateTimeOffset}")]
[HttpGet]
public async Task<ActionResult> ControllerAction(DateTimeOffset dateParam)
{
    //blah
}

These routes both return an InvalidOperationException: The constraint reference 'DateTimeOffset' could not be resolved to a type. Register the constraint type with 'Microsoft.AspNetCore.Routing.RouteOptions.ConstraintMap'.

To try and fix this I added and registered a constraint:

//Constraint
public class DateTimeOffsetConstraint : IRouteConstraint
{
    public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values,
        RouteDirection routeDirection)
    {
        if (values.TryGetValue(routeKey, out var value) && value != null)
        {
            return value is DateTimeOffset;
        }
        return false;
    }
}

//In startup configure services
services.Configure<RouteOptions>(opt => opt.ConstraintMap.Add("DateTimeOffsetConstraint", typeof(DateTimeOffsetConstraint)));

//In controller, altered second route to use constraint
[Route("api/Controller/Action/{dateParam:DateTimeOffsetConstraint}")]
[HttpGet]
public async Task<ActionResult> ControllerAction(DateTimeOffset dateParam)
{
    //blah
}

After that change calling the first route returns InvalidOperationException: Unable to resolve service for type 'System.DateTimeOffset', while the second with the DateTimeOffset (in the proper Zulu time json format e.g. 2019-10-02T05:04:18.070Z) returns a 404.

Following questions by @Kirk Larkin... The controller takes an IDatePeriodRepository, this is defined in another project. This at one point has a DateTimeOffset passed into the constructor

public interface IDatePeriodRepository
{
    Task<int> GetDatePeriod();
    Task<int> GetDatePeriod(DateTimeOffset date);
}

//Defined in seperate file
internal class DatePeriodRepository: IDatePeriodRepository
{
    private readonly DateTimeOffset _dateCycleStart;

    public DatePeriodRepository(DateTimeOffset dateCycleStart)
    {
        _dateCycleStart = dateCycleStart;
    }

    public Task<int> GetDatePeriod()
    {
        return GetDatePeriod(DateTimeOffset.Now);
    }

    public Task<int> GetDatePeriod(DateTimeOffset date)
    {
        var yearDiff = (date.Year - _billingCycleStart.Year) * 12;
        var monthDiff = yearDiff + date.Month - _dateCycleStart.Month;
        return Task.FromResult(monthDiff);
    }
}

This is constructed with a service collection extension

public static class ServiceCollectionExtension
{
    public static IServiceCollection AddDatePeriodRepository(this IServiceCollection services, Action<Options> configuration)
    {
        var options = new Options();
        configuration(options);
        services.AddSingleton(options);
        services.Configure(configuration);

        return services.AddScoped<IDatePeriodRepository, DatePeriodRepository>();
    }
}

//Used in startup ConfigureServices
services.AddBillingPeriodRepository(opt =>
        opt.BillingPeriodCycleStart = Configuration.GetValue<DateTimeOffset>("BillingPeriodCycleStart"));

How can I have a DateTimeOffset as a route attribute?

Smudge202'a answer worked, but I also had to alter the constrinat match method like so:

public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values,
            RouteDirection routeDirection)
{
    if (values.TryGetValue(routeKey, out var value) && value != null)
    {
        return DateTimeOffset.TryParse(value.ToString(), out _);
    }
    return false;
}

Upvotes: 1

Views: 1042

Answers (1)

Smudge202
Smudge202

Reputation: 4687

You're injecting a DateTimeOffset into DatePeriodRepository, however your DI setup is configuring an Options class.

Change DatePeriodRepository to expect the configured Options class:

internal class DatePeriodRepository: IDatePeriodRepository
{
    private readonly DateTimeOffset _dateCycleStart;

    public DatePeriodRepository(Options options)
    {
        _dateCycleStart = options.BillingPeriodCycleStart;
    }

    public Task<int> GetDatePeriod()
    {
        return GetDatePeriod(DateTimeOffset.Now);
    }

    public Task<int> GetDatePeriod(DateTimeOffset date)
    {
        var yearDiff = (date.Year - _billingCycleStart.Year) * 12;
        var monthDiff = yearDiff + date.Month - _dateCycleStart.Month;
        return Task.FromResult(monthDiff);
    }
}

Upvotes: 2

Related Questions