Preamble

I am not a lawyer and nothing in this post should be considered as legal advice, you must make your own determination on how best to comply with legal requirements of your own websites.

The latest ASP.NET project templates for Razor Pages and MVC provide some built in tools to help you meet the GDPR (EU General Data Protection Regulation)  requirements.

Specifically the templates provide:

  • a way to prompt for cookie consent and track cookie consent and to block non-essential first party cookies until consent is granted.
  • a way for users to download and delete the personal data captured by ASP.NET Core Identity if a user registers on the site

Note that it is not a complete solution for meeting GDPR so you should not assume that it provides GDPR compliance out of the box, there are a lot of aspects to consider in meeting the requirements. Some important additional considerations that you must address yourself include handling third party cookies and explaining your use of third party cookies in your privacy policy. It is up to you to make sure no third party cookies are used until after consent has been granted. It is up to you to create a privacy policy that explains what data you and/or your third party partners capture, and to provide mechanisms for users to download or delete any personal data that you capture. There are probably also other requirements that I have not mentioned or haven't yet learned about myself, I cannot emphasize enough that I am not an expert on GDPR and I am not a lawyer.

One thing that I thought should probably be in the template is a mechanism for users to revoke cookie consent after they have granted it. In my layman's understanding of GDPR I interpret that as one of the requirements. In this post I will show you how you can easily add that to your web project built from the ASP.NET Core Razor Page template, the process would be very similar if you use the ASP.NET Core MVC template.

Creating the Project in Visual Studio

In Visual Studio 2017 you create a new ASP.NET Core Web Application as shown in these screen shots.

 screen shot first dialog to create a web app in visual studio

 screen shot, second dialog with options to create a web application in visual studiio

When you run the project, you will immediately see that the cookie consent prompt blocks the navigation bar so you can't login or register or view the menu until cookie consent is granted.

 screen shot showing cookie consent prompt

It is intended for you to customize the consent prompt with a summary of your privacy policy and the "Learn More" button should link to the details of your privacy policy. You can edit the partial view Views/Shared/_CookieConsentPartial.cshtml to customize the summary, and you can edit the Pages/Privacy.cshtml to provide the details of your privacy policy.

Once you click the accept button a cookie is set that indicates your consent and the prompt is hidden to reveal the main navigation bar. But at this point there is no way to change your mind and revoke the cookie consent, that is the part we will add next.

Adding Revoke Cookie Consent Capability

Since the template stubs out a Privacy.cshtml and a corresponding Privacy.cshtml.cs file, I think that is a good place to put the method for revoking consent. You can edit the Privacy.cshtml.cs file and add a post method to revoke consent as shown below.

using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace WebApp.Pages
{
    public class PrivacyModel : PageModel
    {
        public void OnGet()
        {
        }

        public IActionResult OnPostWithdraw()
        {
            HttpContext.Features.Get<ITrackingConsentFeature>().WithdrawConsent();
            return RedirectToPage("./Index");
        }

    }
}

Next you can create a Razor partial view with a form that posts to that method to revoke cookie consent. 

I named it _CookieConsentRevokePartial.cshtml, and it has the following code.

@using Microsoft.AspNetCore.Http.Features
@{
    bool showRevoke = false;
    var consentFeature = Context.Features.Get<ITrackingConsentFeature>();
    if (consentFeature != null && consentFeature.IsConsentNeeded)
    {
        showRevoke = consentFeature.CanTrack;
    }
}
@if (showRevoke)
{
    <form style="display:inline" asp-page="/Privacy" asp-page-handler="Withdraw"  method="post">
         <button type="submit" class="btn btn-link">Revoke Cookie Consent</button>
    </form>
}

I put some hard coded style there to make the form not be a block element because I want to put it in the footer right next to a link to the privacy policy. In your project it is probably better to put that in a CSS stylesheet.

I added the partial view into the _Layout.cshtml file in the footer along with a link to the privacy policy like this:

<footer>
     <div>&copy; 2018 - WebApp   <a asp-page="/Privacy">Privacy Policy</a> <partial name="_CookieConsentRevokePartial" /></div>
</footer>

Now you should see it at the bottom of the page on the next page request after granting consent.

Clicking the "Revoke Cookie Consent" button (styled as a link) will submit the form and revoke consent, and then the cookie consent prompt should re-appear.

That completes what I wanted to show you in this post but do keep in mind there is a lot more to GDPR and you should do research and consult with your legal counsel to determine if your website or web application is meeting the requirements.

See also the Microsoft documentation about GDPR and ASP.NET Core.

Shameless Plug!

I'm the primary developer of cloudscribe, a set of open source ASP.NET components that jump start web application development. Instead of using the standard ASP.NET Core web application templates which don't provide any mechanisms for user management or role management or creating an administrative user, I recommend that you try our cloudscribe project template for Visual Studio, or our cloudscribe project template for dotnet new command line.  Our template provides comprehensive management for users roles, and a whole lot more, and we also have a pretty awesome user friendly content and blog engine. Our template has similar GDPR tools including built in support for revoking cookie consent. With our project templates you can pick and choose which cloudscribe features you want and it will generate a web application with the nuget packages for those features all wired up for you. Please give it a try and see for yourself how much work it can save you because you don't need to write all that code yourself.

Huge thanks to Dave Rael for having me on the Developer on Fire Podcast! I am honored to have been able to talk with Dave on his show. The Developer on Fire Podcast is really unique because it focuses on the people more than on the technology. There is some tech talk of course but he really tries to get at the stories, the personality, what makes you tick, who you are, what motivates you and how you engage with your work. The show has been around for a long time and he has had some real luminaries among his guests including many of my personal tech heroes. I highly recommend adding this podcast to your listening routine if you haven't already.

developer on fire logo

Historically I've always been a database guy, I never used Entity Framework until I started working with ASP.NET Core and implementing it for my cloudscribe projects. Recently I began getting cloudscribe ready for ASP.NET Core 2.0 and Entity Framework Core 2.0, and in the process I learned that some of my "database guy" assumptions about using Entity Framework were wrong. I also learned about some things that need to be done differently when using Entity Framework Core 2.0 with ASP.NET Core 2.0 vs how things were done in 1.x. In this post I will share what I have learned in hopes it may help others.

Avoid Using Database Default Values

As a "database guy", it seemed natural to me to want to specify default values in the database, but when you do that with Entity Framework there are some important nuances that in general make that a bad idea. In most cases you should instead use default values on the entity like this:

public bool AllowNewRegistration { get; set; } = true;

I was doing that, but as a "database guy", my instinct was to also make that the default in the database by specifying a default value in OnModelCreating of my DbContext like this:

entity.Property(p => p.AllowNewRegistration)
    .IsRequired()
    .HasColumnType("bit")
    .HasDefaultValue(true);

However, after updating to Entity Framework Core 2.0-preview2, I began seeing warnings being logged and warnings when generating a new migration like this:

The 'bool' property 'AllowNewRegistration' on entity type 'SiteSettings' is configured with a database-generated default. This default will always be used when the property has the value 'false', since this is the CLR default for the 'bool' type. Consider using the nullable 'bool?' type instead so that the default will only be used when the property value is 'null'.

This kind of scared me at first because I thought it was a change in behavior from Entity Framework Core 1.x to 2.x, but in fact it turned out the behavior was the same in 1.x, and it is only the warnings that are new in 2.0. It scared me because it sounded like any time my entity value was false, it would use the database default of true, and that is what it does but only on inserts, on updates it respects whatever the property is on the entity. There is a bit of nuance in understanding this warning. For example if you have a bool property on the entity and you use a default value of false in the database you would still get the warning but there isn't really a problem because if the entity value is true at insert time you get the expected result, it will be true in the database after insert. The trouble comes when you specify a default value of true in the database, since the CLR type bool has a default value of false, if the entity has false for that property at insert time it will get the database default of true rather than the value that was set on the entity. This may or may not be a real problem in the application depending on whether that property is surfaced in the UI for creating the entity. If the property is only editable for updating the entity and not for creating the entity, then you still get the expected results. But if it is a property that you surface in the UI in order to allow it to be specified at creation time, then you will get the unexpected result if it is set to false, then the database default will be applied.

So, a rule of thumb should be do not specify default values in the database. As an aside I would also say that in general you do not need to specify the ColumnType as I did above, though it causes no problems or warnings, you can generally trust the Entity Framework provider to decide the right data type for the database.

Exceptions to this rule do exist

Ok, so should we never specify a default value? Never say never. Given what we now understand about how default values are used on inserts, we need to consider how to handle it when we add a new bool property to the entity with a default value of true on the entity itself, what will happen to existing rows when the migration is applied and no default value is specified in the db. It turns out the existing rows will not get the entity default of true but will instead get the CLR default of false. This is not what we want.

In this case the solution is to go ahead and specify a default value of true in the database, generate a new migration, then remove the default value and generate another migration. This way the existing rows will get true from the first migration which is what we want, then the second migration will remove the database default value so that we get expected results on new inserts.

Do Specify MaxLength Where Appropriate

This I learned when I first began using Entity Framework Core, not as part of updating from 1.x to 2.x, but it is worth mentioning for anyone new to using Entity Framework. When you have string properties on your entity, if you don't specify a MaxLength then NVarChar(max) will be used in SqlServer or text in other database platforms, so unless you need that much space for the value you should always specify a MaxLength. Note that nvarchar(max) won't use more space than needed but it is still a good idea to limit the size to what you really need, and probably even more important when using other providers than SqlServer.

Other Changes From Entity Framework Core 1.x to 2.x

There are a few other things I ran into when updating cloudscribe to Entity Framework Core 2. These are things that may or may not impact upgrading your own application depending on whether you did any of the same things I did when using 1.x.

I was using some of the provider specific annotations like this:

 modelBuilder.Entity<SiteSettings>(entity =>
 {
    entity.ForSqlServerToTable("cs_Site");

    entity.HasKey(p => p.Id);

    entity.Property(p => p.Id)
       .ForSqlServerHasColumnType("uniqueidentifier")
       .ForSqlServerHasDefaultValueSql("newid()");

    ...
}

Those extensions went away in 2.0 so now we just use the non-provider-specific ones like this:

modelBuilder.Entity<SiteSettings>(entity =>
 {
    entity.ToTable("cs_Site");

    entity.HasKey(p => p.Id);

    entity.Property(p => p.Id)
       .HasColumnType("uniqueidentifier")
       .HasDefaultValueSql("newid()");

    ...
}

Hopefully you were not using the provider specific ones and won't run into that problem yourself. For 2.0-preview2 I had to manually edit my existing migration code to make it more consistent with how they would have been generated using the more generic annotations. I have heard that in 2.0 RTM they will handle some of that automatically but you would still have to make changes for the methods which no longer exist.

I was also wiring up the dependency injection like this in 1.x, though I am not sure it was required to do it this way so it may not impact you. I based my code on examples I found which may date back to the beta  or RC days, but in any case this was working for me in 1.x and caused a problem in 2.x:

services.AddEntityFrameworkSqlServer()
    .AddDbContext<CoreDbContext>((serviceProvider, options) =>
        options.UseSqlServer(connectionString)
               .UseInternalServiceProvider(serviceProvider)
               );

I ran into a problem where an error was thrown when trying to execute the migrations, I was getting the error:

AggregateException: One or more errors occurred. (Cannot consume scoped service 'Microsoft.EntityFrameworkCore.Infrastructure.ModelCustomizerDependencies' from singleton 'Microsoft.EntityFrameworkCore.Infrastructure.IModelCustomizer'.)

There were a few things I changed which made this error go away, first I changed the above code like this:

services.AddEntityFrameworkSqlServer()
    .AddDbContext<CoreDbContext>(options =>
    {
        options.UseSqlServer(connectionString);
    });

getting rid of the UseInternalServiceProvider. Possibly that solved it but I also did another thing which could have been a factor in the fix. Based on observations in other projects using 2.0, I noticed that DbContext should apparently now have a protected constructor that has no parameters, whereas mine only had the constructor that takes a DbContextOptions parameter, so I added an additional constructor like this:

protected CoreDbContext() { }

whereas previously I only had this one:

public CoreDbContext(DbContextOptions<CoreDbContext> options) : base(options) {}

After doing those things that error went away and things were working fine.

However from following some github issues, I learned another thing that is recommended for 2.0 is not to trigger migrations/seeding from the Configure method of Startup.cs which is how it was generally done in 1.x. I had code like this in my Configure Method:

CoreEFStartup.InitializeDatabaseAsync(app.ApplicationServices).Wait();
LoggingEFStartup.InitializeDatabaseAsync(app.ApplicationServices).Wait();
SimpleContentEFStartup.InitializeDatabaseAsync(app.ApplicationServices).Wait();

The above code worked still in 2.x for my scenario, but the new guidance is to move that stuff into Program.cs, so I removed those lines and now my 2.0 Program.cs is like this:

public class Program
{
	public static void Main(string[] args)
	{
		var host = BuildWebHost(args);
		
		using (var scope = host.Services.CreateScope())
		{
			var services = scope.ServiceProvider;

			try
			{
				EnsureDataStorageIsReady(services);

			}
			catch (Exception ex)
			{
				var logger = services.GetRequiredService<ILogger<Program>>();
				logger.LogError(ex, "An error occurred while migrating the database.");
			}
		}

		host.Run();
	}

	public static IWebHost BuildWebHost(string[] args) =>
		WebHost.CreateDefaultBuilder(args)
			.UseStartup<Startup>()
			.Build();

	private static void EnsureDataStorageIsReady(IServiceProvider services)
	{
		CoreEFStartup.InitializeDatabaseAsync(services).Wait();
		SimpleContentEFStartup.InitializeDatabaseAsync(services).Wait();
		LoggingEFStartup.InitializeDatabaseAsync(services).Wait();
	}

}

In summary, a few things have changed in Entity Framework Core 2.x that may or may not impact your applications, but I thought I should make note of the issues I encountered in case some of you encounter the same issues.